Compare commits
No commits in common. "7027252a84e179003d9b9fad3610cfe785d3e292" and "538af5b799191415f1410bdc5b4c1d2f0dd36dd4" have entirely different histories.
7027252a84
...
538af5b799
5 changed files with 37 additions and 36 deletions
|
|
@ -1090,6 +1090,15 @@ def _execute_department_head_step(
|
||||||
role = step["role"]
|
role = step["role"]
|
||||||
dept_name = role.replace("_head", "")
|
dept_name = role.replace("_head", "")
|
||||||
|
|
||||||
|
# Create child pipeline in DB
|
||||||
|
child_pipeline = models.create_pipeline(
|
||||||
|
conn, task_id, project_id,
|
||||||
|
route_type="dept_sub",
|
||||||
|
steps=sub_pipeline,
|
||||||
|
parent_pipeline_id=parent_pipeline_id,
|
||||||
|
department=dept_name,
|
||||||
|
)
|
||||||
|
|
||||||
# Build initial context for workers: dept head's plan + artifacts
|
# Build initial context for workers: dept head's plan + artifacts
|
||||||
dept_plan_context = json.dumps({
|
dept_plan_context = json.dumps({
|
||||||
"department_head_plan": {
|
"department_head_plan": {
|
||||||
|
|
@ -1100,16 +1109,12 @@ def _execute_department_head_step(
|
||||||
}, ensure_ascii=False)
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
# Run the sub-pipeline (noninteractive=True — Opus already reviewed the plan)
|
# Run the sub-pipeline (noninteractive=True — Opus already reviewed the plan)
|
||||||
# pass parent_pipeline_id and department so run_pipeline creates the child
|
|
||||||
# pipeline with correct attributes (route_type='dept_sub') — no double create
|
|
||||||
sub_result = run_pipeline(
|
sub_result = run_pipeline(
|
||||||
conn, task_id, sub_pipeline,
|
conn, task_id, sub_pipeline,
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
allow_write=allow_write,
|
allow_write=allow_write,
|
||||||
noninteractive=True,
|
noninteractive=True,
|
||||||
initial_previous_output=dept_plan_context,
|
initial_previous_output=dept_plan_context,
|
||||||
parent_pipeline_id=parent_pipeline_id,
|
|
||||||
department=dept_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract decisions from sub-pipeline results for handoff
|
# Extract decisions from sub-pipeline results for handoff
|
||||||
|
|
|
||||||
22
core/db.py
22
core/db.py
|
|
@ -612,18 +612,16 @@ def _migrate(conn: sqlite3.Connection):
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# Migrate pipelines: add parent_pipeline_id and department columns (KIN-098)
|
# Migrate pipelines: add parent_pipeline_id and department columns (KIN-098)
|
||||||
# Guard: table may not exist in legacy schemas without pipelines (old test fixtures)
|
pipeline_cols = {r[1] for r in conn.execute("PRAGMA table_info(pipelines)").fetchall()}
|
||||||
if "pipelines" in existing_tables:
|
if "parent_pipeline_id" not in pipeline_cols:
|
||||||
pipeline_cols = {r[1] for r in conn.execute("PRAGMA table_info(pipelines)").fetchall()}
|
conn.execute("ALTER TABLE pipelines ADD COLUMN parent_pipeline_id INTEGER REFERENCES pipelines(id)")
|
||||||
if "parent_pipeline_id" not in pipeline_cols:
|
conn.commit()
|
||||||
conn.execute("ALTER TABLE pipelines ADD COLUMN parent_pipeline_id INTEGER REFERENCES pipelines(id)")
|
if "department" not in pipeline_cols:
|
||||||
conn.commit()
|
conn.execute("ALTER TABLE pipelines ADD COLUMN department TEXT")
|
||||||
if "department" not in pipeline_cols:
|
conn.commit()
|
||||||
conn.execute("ALTER TABLE pipelines ADD COLUMN department TEXT")
|
if "pid" not in pipeline_cols:
|
||||||
conn.commit()
|
conn.execute("ALTER TABLE pipelines ADD COLUMN pid INTEGER")
|
||||||
if "pid" not in pipeline_cols:
|
conn.commit()
|
||||||
conn.execute("ALTER TABLE pipelines ADD COLUMN pid INTEGER")
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# Create department_handoffs table (KIN-098)
|
# Create department_handoffs table (KIN-098)
|
||||||
if "department_handoffs" not in existing_tables:
|
if "department_handoffs" not in existing_tables:
|
||||||
|
|
|
||||||
|
|
@ -394,19 +394,22 @@ class TestPmPromptStatusFieldConsistency:
|
||||||
"Issue 3 has been fixed — update or remove this test."
|
"Issue 3 has been fixed — update or remove this test."
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_pm_prompt_does_not_mention_status_values_after_example(self):
|
def test_pm_prompt_mentions_status_values_after_example(self):
|
||||||
"""Orphaned 'Valid values for status' line was removed from pm.md.
|
"""Line after main example mentions 'Valid values for status: done, blocked'.
|
||||||
|
|
||||||
Issue 3 fixed: the ambiguous note that appeared after the main JSON example
|
Documents that this note appears AFTER the main example, causing ambiguity.
|
||||||
and referenced a 'status' field absent from that example has been deleted.
|
|
||||||
"""
|
"""
|
||||||
with open("agents/prompts/pm.md") as f:
|
with open("agents/prompts/pm.md") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
assert 'Valid values for `status`' not in content, (
|
assert 'Valid values for `status`' in content, (
|
||||||
"Orphaned 'Valid values for `status`' line was re-introduced in pm.md"
|
"Expected 'Valid values for `status`' in pm.md — did the prompt change?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Confirm it references done and blocked
|
||||||
|
assert '"done"' in content
|
||||||
|
assert '"blocked"' in content
|
||||||
|
|
||||||
def test_pm_blocked_protocol_example_uses_reason_not_blocked_reason(self):
|
def test_pm_blocked_protocol_example_uses_reason_not_blocked_reason(self):
|
||||||
"""The Blocked Protocol JSON example uses 'reason' (not 'blocked_reason').
|
"""The Blocked Protocol JSON example uses 'reason' (not 'blocked_reason').
|
||||||
|
|
||||||
|
|
@ -518,11 +521,10 @@ class TestContextBuilderDuplicateAssignment:
|
||||||
assert "PROJ-001" in result
|
assert "PROJ-001" in result
|
||||||
assert "My test task" in result
|
assert "My test task" in result
|
||||||
|
|
||||||
def test_source_code_has_single_task_assignment(self):
|
def test_source_code_has_duplicate_task_assignment(self):
|
||||||
"""Duplicate assignment was removed from context_builder.py.
|
"""Documents that the duplicate assignment still exists in the source.
|
||||||
|
|
||||||
Issue 4 fixed: task = context.get('task') now appears exactly once
|
This test will FAIL once the duplicate is removed (cleanup completed).
|
||||||
inside format_prompt().
|
|
||||||
"""
|
"""
|
||||||
import ast
|
import ast
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
@ -534,12 +536,14 @@ class TestContextBuilderDuplicateAssignment:
|
||||||
task_assign_count = 0
|
task_assign_count = 0
|
||||||
for node in ast.walk(tree):
|
for node in ast.walk(tree):
|
||||||
if isinstance(node, ast.Assign):
|
if isinstance(node, ast.Assign):
|
||||||
|
# Check target is 'task'
|
||||||
targets_are_task = any(
|
targets_are_task = any(
|
||||||
isinstance(t, ast.Name) and t.id == "task"
|
isinstance(t, ast.Name) and t.id == "task"
|
||||||
for t in node.targets
|
for t in node.targets
|
||||||
)
|
)
|
||||||
if not targets_are_task:
|
if not targets_are_task:
|
||||||
continue
|
continue
|
||||||
|
# Check value is context.get('task') or context.get("task")
|
||||||
val = node.value
|
val = node.value
|
||||||
if (
|
if (
|
||||||
isinstance(val, ast.Call)
|
isinstance(val, ast.Call)
|
||||||
|
|
@ -553,7 +557,8 @@ class TestContextBuilderDuplicateAssignment:
|
||||||
):
|
):
|
||||||
task_assign_count += 1
|
task_assign_count += 1
|
||||||
|
|
||||||
assert task_assign_count == 1, (
|
assert task_assign_count == 2, (
|
||||||
f"Expected exactly 1 'task = context.get(\"task\")' assignment, "
|
f"Expected exactly 2 duplicate 'task = context.get(\"task\")' assignments "
|
||||||
f"found {task_assign_count}. Duplicate may have been re-introduced."
|
f"(documenting Issue 4), found {task_assign_count}. "
|
||||||
|
"If count is 1, the duplicate was removed — this test can be deleted."
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ export interface Project {
|
||||||
tech_stack: string[] | null
|
tech_stack: string[] | null
|
||||||
execution_mode: string | null
|
execution_mode: string | null
|
||||||
autocommit_enabled: number | null
|
autocommit_enabled: number | null
|
||||||
auto_test_enabled: number | null
|
|
||||||
obsidian_vault_path: string | null
|
obsidian_vault_path: string | null
|
||||||
deploy_command: string | null
|
deploy_command: string | null
|
||||||
test_command: string | null
|
test_command: string | null
|
||||||
|
|
|
||||||
|
|
@ -696,12 +696,6 @@ async function addDecision() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload warning banner -->
|
|
||||||
<div v-if="uploadWarning" class="mb-4 px-4 py-3 border border-yellow-700 bg-yellow-950/30 rounded flex items-start justify-between gap-2">
|
|
||||||
<p class="text-sm text-yellow-300">⚠ {{ uploadWarning }}</p>
|
|
||||||
<button @click="uploadWarning = ''" class="text-gray-600 hover:text-gray-400 bg-transparent border-none cursor-pointer text-xs shrink-0">✕</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="flex gap-1 mb-4 border-b border-gray-800">
|
<div class="flex gap-1 mb-4 border-b border-gray-800">
|
||||||
<button v-for="tab in (['tasks', 'phases', 'decisions', 'modules', 'kanban', 'environments'] as const)" :key="tab"
|
<button v-for="tab in (['tasks', 'phases', 'decisions', 'modules', 'kanban', 'environments'] as const)" :key="tab"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue