@blxzer/cursor-trellis 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +13 -13
- package/README.zh-CN.md +1 -1
- package/bin/cstl.js +1 -1
- package/bin/smart-search.js +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +17 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/campaign/canvas-render.d.ts +68 -0
- package/dist/commands/campaign/canvas-render.d.ts.map +1 -0
- package/dist/commands/campaign/canvas-render.js +414 -0
- package/dist/commands/campaign/canvas-render.js.map +1 -0
- package/dist/commands/campaign/compose.d.ts +15 -0
- package/dist/commands/campaign/compose.d.ts.map +1 -0
- package/dist/commands/campaign/compose.js +110 -0
- package/dist/commands/campaign/compose.js.map +1 -0
- package/dist/commands/campaign/index.d.ts +13 -0
- package/dist/commands/campaign/index.d.ts.map +1 -0
- package/dist/commands/campaign/index.js +134 -0
- package/dist/commands/campaign/index.js.map +1 -0
- package/dist/commands/campaign/kind-map.d.ts +6 -0
- package/dist/commands/campaign/kind-map.d.ts.map +1 -0
- package/dist/commands/campaign/kind-map.js +21 -0
- package/dist/commands/campaign/kind-map.js.map +1 -0
- package/dist/commands/campaign/mcp-server.d.ts +6 -0
- package/dist/commands/campaign/mcp-server.d.ts.map +1 -0
- package/dist/commands/campaign/mcp-server.js +167 -0
- package/dist/commands/campaign/mcp-server.js.map +1 -0
- package/dist/commands/campaign/render.d.ts +5 -0
- package/dist/commands/campaign/render.d.ts.map +1 -0
- package/dist/commands/campaign/render.js +103 -0
- package/dist/commands/campaign/render.js.map +1 -0
- package/dist/commands/campaign/trellis-load.d.ts +13 -0
- package/dist/commands/campaign/trellis-load.d.ts.map +1 -0
- package/dist/commands/campaign/trellis-load.js +122 -0
- package/dist/commands/campaign/trellis-load.js.map +1 -0
- package/dist/commands/campaign/types.d.ts +61 -0
- package/dist/commands/campaign/types.d.ts.map +1 -0
- package/dist/commands/campaign/types.js +3 -0
- package/dist/commands/campaign/types.js.map +1 -0
- package/dist/commands/goal/index.d.ts +3 -0
- package/dist/commands/goal/index.d.ts.map +1 -0
- package/dist/commands/goal/index.js +159 -0
- package/dist/commands/goal/index.js.map +1 -0
- package/dist/commands/goal/review.d.ts +6 -0
- package/dist/commands/goal/review.d.ts.map +1 -0
- package/dist/commands/goal/review.js +28 -0
- package/dist/commands/goal/review.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/rpc/client.d.ts +51 -0
- package/dist/commands/rpc/client.d.ts.map +1 -0
- package/dist/commands/rpc/client.js +147 -0
- package/dist/commands/rpc/client.js.map +1 -0
- package/dist/commands/rpc/index.d.ts +10 -0
- package/dist/commands/rpc/index.d.ts.map +1 -0
- package/dist/commands/rpc/index.js +157 -0
- package/dist/commands/rpc/index.js.map +1 -0
- package/dist/commands/rpc/serve.d.ts +23 -0
- package/dist/commands/rpc/serve.d.ts.map +1 -0
- package/dist/commands/rpc/serve.js +60 -0
- package/dist/commands/rpc/serve.js.map +1 -0
- package/dist/commands/rpc/smoke.d.ts +16 -0
- package/dist/commands/rpc/smoke.d.ts.map +1 -0
- package/dist/commands/rpc/smoke.js +92 -0
- package/dist/commands/rpc/smoke.js.map +1 -0
- package/dist/commands/sdk/index.d.ts +9 -0
- package/dist/commands/sdk/index.d.ts.map +1 -0
- package/dist/commands/sdk/index.js +58 -0
- package/dist/commands/sdk/index.js.map +1 -0
- package/dist/commands/sdk/run.d.ts +42 -0
- package/dist/commands/sdk/run.d.ts.map +1 -0
- package/dist/commands/sdk/run.js +220 -0
- package/dist/commands/sdk/run.js.map +1 -0
- package/dist/commands/sdk/status.d.ts +14 -0
- package/dist/commands/sdk/status.d.ts.map +1 -0
- package/dist/commands/sdk/status.js +69 -0
- package/dist/commands/sdk/status.js.map +1 -0
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +13 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/goal/action-packet.d.ts +89 -0
- package/dist/goal/action-packet.d.ts.map +1 -0
- package/dist/goal/action-packet.js +113 -0
- package/dist/goal/action-packet.js.map +1 -0
- package/dist/goal/audit.d.ts +14 -0
- package/dist/goal/audit.d.ts.map +1 -0
- package/dist/goal/audit.js +12 -0
- package/dist/goal/audit.js.map +1 -0
- package/dist/goal/constants.d.ts +26 -0
- package/dist/goal/constants.d.ts.map +1 -0
- package/dist/goal/constants.js +24 -0
- package/dist/goal/constants.js.map +1 -0
- package/dist/goal/paths.d.ts +8 -0
- package/dist/goal/paths.d.ts.map +1 -0
- package/dist/goal/paths.js +36 -0
- package/dist/goal/paths.js.map +1 -0
- package/dist/goal/preflight.d.ts +21 -0
- package/dist/goal/preflight.d.ts.map +1 -0
- package/dist/goal/preflight.js +97 -0
- package/dist/goal/preflight.js.map +1 -0
- package/dist/goal/review-seam.d.ts +44 -0
- package/dist/goal/review-seam.d.ts.map +1 -0
- package/dist/goal/review-seam.js +153 -0
- package/dist/goal/review-seam.js.map +1 -0
- package/dist/goal/reviewer-rules.d.ts +13 -0
- package/dist/goal/reviewer-rules.d.ts.map +1 -0
- package/dist/goal/reviewer-rules.js +153 -0
- package/dist/goal/reviewer-rules.js.map +1 -0
- package/dist/goal/reviewer.d.ts +9 -0
- package/dist/goal/reviewer.d.ts.map +1 -0
- package/dist/goal/reviewer.js +54 -0
- package/dist/goal/reviewer.js.map +1 -0
- package/dist/goal/runtime.d.ts +32 -0
- package/dist/goal/runtime.d.ts.map +1 -0
- package/dist/goal/runtime.js +242 -0
- package/dist/goal/runtime.js.map +1 -0
- package/dist/goal/sdk-client.d.ts +10 -0
- package/dist/goal/sdk-client.d.ts.map +1 -0
- package/dist/goal/sdk-client.js +34 -0
- package/dist/goal/sdk-client.js.map +1 -0
- package/dist/goal/state.d.ts +40 -0
- package/dist/goal/state.d.ts.map +1 -0
- package/dist/goal/state.js +37 -0
- package/dist/goal/state.js.map +1 -0
- package/dist/goal/task-triggers.d.ts +15 -0
- package/dist/goal/task-triggers.d.ts.map +1 -0
- package/dist/goal/task-triggers.js +23 -0
- package/dist/goal/task-triggers.js.map +1 -0
- package/dist/goal/walls.d.ts +12 -0
- package/dist/goal/walls.d.ts.map +1 -0
- package/dist/goal/walls.js +65 -0
- package/dist/goal/walls.js.map +1 -0
- package/dist/goal/worker-prompt.d.ts +13 -0
- package/dist/goal/worker-prompt.d.ts.map +1 -0
- package/dist/goal/worker-prompt.js +41 -0
- package/dist/goal/worker-prompt.js.map +1 -0
- package/dist/goal/worker.d.ts +42 -0
- package/dist/goal/worker.d.ts.map +1 -0
- package/dist/goal/worker.js +180 -0
- package/dist/goal/worker.js.map +1 -0
- package/dist/migrations/manifests/0.3.5.json +9 -0
- package/dist/migrations/manifests/0.3.6.json +9 -0
- package/dist/templates/common/bundled-skills/smart-search-cli/SKILL.md +35 -29
- package/dist/templates/common/bundled-skills/smart-search-cli/references/cli-contract.md +11 -25
- package/dist/templates/common/commands/continue.md +15 -3
- package/dist/templates/common/skills/check.md +7 -4
- package/dist/templates/cursor/agents/cstl-check.md +1 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
- package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
- package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
- package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
- package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
- package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
- package/dist/templates/markdown/agents.md +2 -0
- package/dist/templates/markdown/index.d.ts +3 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +3 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
- package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +107 -104
- package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +38 -6
- package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +184 -0
- package/dist/templates/shared-hooks/session-start.py +10 -0
- package/dist/templates/trellis/config.yaml +18 -0
- package/dist/templates/trellis/index.d.ts +5 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +14 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +51 -2
- package/dist/templates/trellis/scripts/common/config.py +39 -0
- package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
- package/dist/templates/trellis/scripts/common/parent_orchestration.py +500 -5
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
- package/dist/templates/trellis/scripts/common/session_context.py +32 -4
- package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
- package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
- package/dist/templates/trellis/scripts/common/task_context.py +18 -0
- package/dist/templates/trellis/scripts/common/task_dashboard.py +16 -1
- package/dist/templates/trellis/scripts/common/task_map.py +670 -614
- package/dist/templates/trellis/scripts/common/task_store.py +191 -40
- package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
- package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
- package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
- package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
- package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
- package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
- package/dist/templates/trellis/scripts/task.py +51 -0
- package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
- package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
- package/dist/templates/trellis/workflow.md +23 -3
- package/dist/utils/codebase-retrieval-router.d.ts.map +1 -1
- package/dist/utils/codebase-retrieval-router.js +14 -0
- package/dist/utils/codebase-retrieval-router.js.map +1 -1
- package/dist/utils/cursor-sdk-gate.d.ts +10 -0
- package/dist/utils/cursor-sdk-gate.d.ts.map +1 -0
- package/dist/utils/cursor-sdk-gate.js +32 -0
- package/dist/utils/cursor-sdk-gate.js.map +1 -0
- package/dist/utils/project-capabilities.d.ts +30 -3
- package/dist/utils/project-capabilities.d.ts.map +1 -1
- package/dist/utils/project-capabilities.js +182 -11
- package/dist/utils/project-capabilities.js.map +1 -1
- package/dist/utils/readiness.d.ts.map +1 -1
- package/dist/utils/readiness.js +26 -0
- package/dist/utils/readiness.js.map +1 -1
- package/package.json +4 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
Task CRUD operations.
|
|
4
4
|
|
|
@@ -25,6 +25,11 @@ import sys
|
|
|
25
25
|
from datetime import datetime
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
|
|
28
|
+
from .artifact_locale import (
|
|
29
|
+
default_prd_content,
|
|
30
|
+
default_verify_content,
|
|
31
|
+
resolve_artifact_locale,
|
|
32
|
+
)
|
|
28
33
|
from .config import (
|
|
29
34
|
get_packages,
|
|
30
35
|
get_session_auto_commit,
|
|
@@ -67,6 +72,7 @@ from .task_map import (
|
|
|
67
72
|
get_child_state,
|
|
68
73
|
record_child_worktree,
|
|
69
74
|
remove_child_from_task_map,
|
|
75
|
+
resolve_child_map_id,
|
|
70
76
|
set_child_state,
|
|
71
77
|
set_parent_child_integration_state,
|
|
72
78
|
validate_parent_child_integration,
|
|
@@ -249,11 +255,10 @@ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
|
|
|
249
255
|
".pi", # Pi Agent
|
|
250
256
|
)
|
|
251
257
|
|
|
252
|
-
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"Delete this line once real entries are added."
|
|
258
|
+
_STABLE_SPEC_SEED_PATHS: tuple[str, ...] = (
|
|
259
|
+
".cstl/spec/guides/index.md",
|
|
260
|
+
".cstl/spec/guides/verification-strength-guide.md",
|
|
261
|
+
".cstl/spec/guides/injection-budget-guide.md",
|
|
257
262
|
)
|
|
258
263
|
|
|
259
264
|
|
|
@@ -270,41 +275,70 @@ def _has_subagent_platform(repo_root: Path) -> bool:
|
|
|
270
275
|
return False
|
|
271
276
|
|
|
272
277
|
|
|
273
|
-
def
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
278
|
+
def _resolve_seed_spec_paths(
|
|
279
|
+
repo_root: Path,
|
|
280
|
+
task_dir: Path,
|
|
281
|
+
task_data: dict,
|
|
282
|
+
) -> list[str]:
|
|
283
|
+
"""Return 2–3 existing spec file paths for jsonl create seeds."""
|
|
284
|
+
from .task_gates import suggest_spec_targets
|
|
285
|
+
|
|
286
|
+
candidates: list[str] = []
|
|
287
|
+
for path in suggest_spec_targets(repo_root, task_dir, task_data):
|
|
288
|
+
if path not in candidates:
|
|
289
|
+
candidates.append(path)
|
|
290
|
+
for rel in _STABLE_SPEC_SEED_PATHS:
|
|
291
|
+
if rel not in candidates:
|
|
292
|
+
candidates.append(rel)
|
|
293
|
+
|
|
294
|
+
existing = [rel for rel in candidates if (repo_root / rel).is_file()]
|
|
295
|
+
if len(existing) >= 2:
|
|
296
|
+
return existing[:3]
|
|
297
|
+
|
|
298
|
+
fallback = [
|
|
299
|
+
rel for rel in _STABLE_SPEC_SEED_PATHS if (repo_root / rel).is_file()
|
|
300
|
+
]
|
|
301
|
+
return fallback[:3]
|
|
295
302
|
|
|
296
|
-
- TBD
|
|
297
303
|
|
|
298
|
-
|
|
304
|
+
def _write_seed_jsonl(
|
|
305
|
+
path: Path,
|
|
306
|
+
repo_root: Path,
|
|
307
|
+
task_dir: Path,
|
|
308
|
+
task_data: dict,
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Write 2–3 JSONL rows with real spec ``file`` paths for sub-agent context."""
|
|
311
|
+
spec_paths = _resolve_seed_spec_paths(repo_root, task_dir, task_data)
|
|
312
|
+
if len(spec_paths) < 2:
|
|
313
|
+
raise ValueError(
|
|
314
|
+
"task create requires at least two existing spec guide paths for jsonl seed"
|
|
315
|
+
)
|
|
299
316
|
|
|
300
|
-
|
|
317
|
+
lines: list[str] = []
|
|
318
|
+
for spec_path in spec_paths:
|
|
319
|
+
lines.append(
|
|
320
|
+
json.dumps(
|
|
321
|
+
{
|
|
322
|
+
"file": spec_path,
|
|
323
|
+
"reason": "default spec seed — curate entries for this task",
|
|
324
|
+
},
|
|
325
|
+
ensure_ascii=False,
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
301
329
|
|
|
302
|
-
## Notes
|
|
303
330
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
331
|
+
def _default_prd_content(
|
|
332
|
+
title: str,
|
|
333
|
+
description: str | None = None,
|
|
334
|
+
*,
|
|
335
|
+
repo_root: Path | None = None,
|
|
336
|
+
task_dir: Path | None = None,
|
|
337
|
+
) -> str:
|
|
338
|
+
"""Return the default PRD skeleton created with every task."""
|
|
339
|
+
root = repo_root or get_repo_root()
|
|
340
|
+
locale = resolve_artifact_locale(task_dir, root)
|
|
341
|
+
return default_prd_content(title, description, locale, root)
|
|
308
342
|
|
|
309
343
|
|
|
310
344
|
# =============================================================================
|
|
@@ -412,7 +446,14 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
412
446
|
prd_path = task_dir / "prd.md"
|
|
413
447
|
if not prd_path.exists():
|
|
414
448
|
prd_path.write_text(
|
|
415
|
-
_default_prd_content(args.title, args.description),
|
|
449
|
+
_default_prd_content(args.title, args.description, repo_root=repo_root, task_dir=task_dir),
|
|
450
|
+
encoding="utf-8",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
verify_path = task_dir / "verify.md"
|
|
454
|
+
if not verify_path.exists():
|
|
455
|
+
verify_path.write_text(
|
|
456
|
+
default_verify_content(repo_root=repo_root, task_dir=task_dir),
|
|
416
457
|
encoding="utf-8",
|
|
417
458
|
)
|
|
418
459
|
|
|
@@ -425,7 +466,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
425
466
|
for jsonl_name in ("implement.jsonl", "check.jsonl"):
|
|
426
467
|
jsonl_path = task_dir / jsonl_name
|
|
427
468
|
if not jsonl_path.exists():
|
|
428
|
-
_write_seed_jsonl(jsonl_path)
|
|
469
|
+
_write_seed_jsonl(jsonl_path, repo_root, task_dir, task_data)
|
|
429
470
|
seeded_jsonl = True
|
|
430
471
|
|
|
431
472
|
# Handle --parent: establish bidirectional link
|
|
@@ -1136,10 +1177,11 @@ def cmd_set_child_state(args: argparse.Namespace) -> int:
|
|
|
1136
1177
|
)
|
|
1137
1178
|
return 1
|
|
1138
1179
|
|
|
1180
|
+
child_map_id = resolve_child_map_id(parent_data, child_dir, child_data)
|
|
1139
1181
|
ok, errors = set_child_state(
|
|
1140
1182
|
parent_dir,
|
|
1141
1183
|
parent_data,
|
|
1142
|
-
|
|
1184
|
+
child_map_id,
|
|
1143
1185
|
state,
|
|
1144
1186
|
args.evidence,
|
|
1145
1187
|
getattr(args, "reason", None),
|
|
@@ -1265,6 +1307,29 @@ def cmd_integrate_child(args: argparse.Namespace) -> int:
|
|
|
1265
1307
|
if merge_ref:
|
|
1266
1308
|
print(f"Merge executed: git merge --no-ff --no-commit {merge_ref}")
|
|
1267
1309
|
print(f"Parent map: {_repo_relative_path(parent_dir / 'task-map.md', repo_root)}")
|
|
1310
|
+
|
|
1311
|
+
if state == "integrated" and not getattr(args, "no_publish_pack", False):
|
|
1312
|
+
from .parent_orchestration import build_publish_pack
|
|
1313
|
+
|
|
1314
|
+
summary, pack_errors = build_publish_pack(parent_dir)
|
|
1315
|
+
if pack_errors or summary is None:
|
|
1316
|
+
for item in pack_errors:
|
|
1317
|
+
print(f" - {item}", file=sys.stderr)
|
|
1318
|
+
print(
|
|
1319
|
+
colored(
|
|
1320
|
+
"Warning: integrate succeeded but publish-pack failed; "
|
|
1321
|
+
"PACK may be stale (see parent-status stalePack).",
|
|
1322
|
+
Colors.YELLOW,
|
|
1323
|
+
),
|
|
1324
|
+
file=sys.stderr,
|
|
1325
|
+
)
|
|
1326
|
+
return 2
|
|
1327
|
+
print(colored("✓ PACK refreshed after integrate", Colors.GREEN))
|
|
1328
|
+
for line in summary.splitlines():
|
|
1329
|
+
if line.startswith("- Generated:"):
|
|
1330
|
+
print(line)
|
|
1331
|
+
break
|
|
1332
|
+
|
|
1268
1333
|
return 0
|
|
1269
1334
|
|
|
1270
1335
|
|
|
@@ -1374,14 +1439,60 @@ def cmd_generate_child_prompt(args: argparse.Namespace) -> int:
|
|
|
1374
1439
|
|
|
1375
1440
|
def cmd_parent_status(args: argparse.Namespace) -> int:
|
|
1376
1441
|
"""Show parent task-map orchestration status."""
|
|
1377
|
-
|
|
1442
|
+
import json
|
|
1443
|
+
|
|
1444
|
+
from .parent_orchestration import build_parent_status, build_parent_status_dict
|
|
1378
1445
|
|
|
1379
1446
|
repo_root = get_repo_root()
|
|
1380
1447
|
parent_dir = resolve_task_dir(args.parent_dir, repo_root)
|
|
1448
|
+
if getattr(args, "json", False):
|
|
1449
|
+
payload = build_parent_status_dict(parent_dir)
|
|
1450
|
+
if payload is None:
|
|
1451
|
+
print(
|
|
1452
|
+
json.dumps(
|
|
1453
|
+
{
|
|
1454
|
+
"error": "task-map.md missing or invalid",
|
|
1455
|
+
"path": str(parent_dir),
|
|
1456
|
+
}
|
|
1457
|
+
)
|
|
1458
|
+
)
|
|
1459
|
+
return 1
|
|
1460
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
1461
|
+
return 0
|
|
1381
1462
|
print(build_parent_status(parent_dir))
|
|
1382
1463
|
return 0
|
|
1383
1464
|
|
|
1384
1465
|
|
|
1466
|
+
|
|
1467
|
+
def cmd_publish_pack(args: argparse.Namespace) -> int:
|
|
1468
|
+
"""Publish HYBRID campaign PACK (ready list + child prompts)."""
|
|
1469
|
+
from .parent_orchestration import build_publish_pack
|
|
1470
|
+
|
|
1471
|
+
repo_root = get_repo_root()
|
|
1472
|
+
parent_dir = resolve_task_dir(args.parent_dir, repo_root)
|
|
1473
|
+
mode = getattr(args, "mode", "inline") or "inline"
|
|
1474
|
+
if mode not in ("inline", "subagent"):
|
|
1475
|
+
print(colored("Error: --mode must be inline or subagent", Colors.RED), file=sys.stderr)
|
|
1476
|
+
return 1
|
|
1477
|
+
|
|
1478
|
+
summary, errors = build_publish_pack(
|
|
1479
|
+
parent_dir,
|
|
1480
|
+
stage_id=getattr(args, "stage", None),
|
|
1481
|
+
mode=mode,
|
|
1482
|
+
dry_run=bool(getattr(args, "dry_run", False)),
|
|
1483
|
+
)
|
|
1484
|
+
if errors:
|
|
1485
|
+
for item in errors:
|
|
1486
|
+
print(f" - {item}", file=sys.stderr)
|
|
1487
|
+
print(colored("Error: publish-pack failed", Colors.RED), file=sys.stderr)
|
|
1488
|
+
return 1
|
|
1489
|
+
if summary is None:
|
|
1490
|
+
print(colored("Error: publish-pack produced no summary", Colors.RED), file=sys.stderr)
|
|
1491
|
+
return 1
|
|
1492
|
+
print(summary)
|
|
1493
|
+
return 0
|
|
1494
|
+
|
|
1495
|
+
|
|
1385
1496
|
def cmd_review_child(args: argparse.Namespace) -> int:
|
|
1386
1497
|
"""Review child handoff and optionally advance parent integration states."""
|
|
1387
1498
|
from .parent_orchestration import (
|
|
@@ -1592,3 +1703,43 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
|
|
|
1592
1703
|
|
|
1593
1704
|
print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
|
|
1594
1705
|
return 0
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
# =============================================================================
|
|
1709
|
+
# Command: artifact-locale
|
|
1710
|
+
# =============================================================================
|
|
1711
|
+
|
|
1712
|
+
def cmd_artifact_locale(args: argparse.Namespace) -> int:
|
|
1713
|
+
"""Get or set human-reviewed artifact locale (zh | en)."""
|
|
1714
|
+
from .artifact_locale import (
|
|
1715
|
+
resolve_artifact_locale,
|
|
1716
|
+
set_task_artifact_locale,
|
|
1717
|
+
set_workspace_artifact_locale,
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
repo_root = get_repo_root()
|
|
1721
|
+
subcommand = getattr(args, "artifact_locale_command", None)
|
|
1722
|
+
|
|
1723
|
+
if subcommand == "get":
|
|
1724
|
+
task_dir = resolve_task_dir(args.task, repo_root) if args.task else None
|
|
1725
|
+
print(resolve_artifact_locale(task_dir, repo_root))
|
|
1726
|
+
return 0
|
|
1727
|
+
|
|
1728
|
+
if subcommand == "set":
|
|
1729
|
+
try:
|
|
1730
|
+
if args.task:
|
|
1731
|
+
task_dir = resolve_task_dir(args.task, repo_root)
|
|
1732
|
+
set_task_artifact_locale(task_dir, args.locale, repo_root)
|
|
1733
|
+
scope = f"task {task_dir.name}"
|
|
1734
|
+
else:
|
|
1735
|
+
set_workspace_artifact_locale(args.locale, repo_root)
|
|
1736
|
+
scope = "workspace"
|
|
1737
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
1738
|
+
print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
|
|
1739
|
+
return 1
|
|
1740
|
+
|
|
1741
|
+
print(colored(f"✓ artifact_locale set to {args.locale} ({scope})", Colors.GREEN))
|
|
1742
|
+
return 0
|
|
1743
|
+
|
|
1744
|
+
print(colored("Error: use artifact-locale get|set", Colors.RED), file=sys.stderr)
|
|
1745
|
+
return 1
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for P1 observable defaults (verify seed, jsonl seed, dashboard summary)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
13
|
+
|
|
14
|
+
from common.artifact_locale import default_verify_content
|
|
15
|
+
from common.task_dashboard import format_verify_summary, render_task_dashboard
|
|
16
|
+
from common.task_store import _resolve_seed_spec_paths, _write_seed_jsonl
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_default_verify_content_has_standard_sections() -> None:
|
|
20
|
+
content = default_verify_content(locale="en")
|
|
21
|
+
for heading in (
|
|
22
|
+
"## Planning check",
|
|
23
|
+
"## Execution evidence",
|
|
24
|
+
"### Validation commands",
|
|
25
|
+
"### Acceptance",
|
|
26
|
+
"### Durable learning",
|
|
27
|
+
):
|
|
28
|
+
assert heading in content
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_write_seed_jsonl_uses_real_spec_paths(tmp_path: Path) -> None:
|
|
32
|
+
repo_root = tmp_path / "repo"
|
|
33
|
+
spec_root = repo_root / ".cstl" / "spec" / "guides"
|
|
34
|
+
spec_root.mkdir(parents=True)
|
|
35
|
+
for name in ("index.md", "verification-strength-guide.md", "injection-budget-guide.md"):
|
|
36
|
+
(spec_root / name).write_text(f"# {name}\n", encoding="utf-8")
|
|
37
|
+
|
|
38
|
+
task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-seed"
|
|
39
|
+
task_dir.mkdir(parents=True)
|
|
40
|
+
task_data = {"id": "test-seed", "package": None, "scope": None, "children": []}
|
|
41
|
+
|
|
42
|
+
jsonl_path = task_dir / "implement.jsonl"
|
|
43
|
+
_write_seed_jsonl(jsonl_path, repo_root, task_dir, task_data)
|
|
44
|
+
|
|
45
|
+
rows = [json.loads(line) for line in jsonl_path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
46
|
+
assert len(rows) >= 2
|
|
47
|
+
assert all("file" in row and row["file"].startswith(".cstl/spec/") for row in rows)
|
|
48
|
+
assert all("_example" not in row for row in rows)
|
|
49
|
+
for row in rows:
|
|
50
|
+
assert (repo_root / row["file"]).is_file()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_resolve_seed_spec_paths_prefers_existing_guides(tmp_path: Path) -> None:
|
|
54
|
+
repo_root = tmp_path / "repo"
|
|
55
|
+
guides = repo_root / ".cstl" / "spec" / "guides"
|
|
56
|
+
guides.mkdir(parents=True)
|
|
57
|
+
(guides / "index.md").write_text("# index\n", encoding="utf-8")
|
|
58
|
+
(guides / "verification-strength-guide.md").write_text("# verify\n", encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-paths"
|
|
61
|
+
task_dir.mkdir(parents=True)
|
|
62
|
+
paths = _resolve_seed_spec_paths(repo_root, task_dir, {"children": []})
|
|
63
|
+
assert len(paths) >= 2
|
|
64
|
+
assert all((repo_root / rel).is_file() for rel in paths)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_format_verify_summary_missing_without_file(tmp_path: Path) -> None:
|
|
68
|
+
task_dir = tmp_path / "task"
|
|
69
|
+
task_dir.mkdir()
|
|
70
|
+
assert format_verify_summary(task_dir, {"children": []}) == "[verify: missing]"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_format_verify_summary_partial_from_signals(tmp_path: Path) -> None:
|
|
74
|
+
task_dir = tmp_path / "task"
|
|
75
|
+
task_dir.mkdir()
|
|
76
|
+
(task_dir / "verify.md").write_text(
|
|
77
|
+
"Validation commands: pytest -q — pass\n",
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
)
|
|
80
|
+
assert format_verify_summary(task_dir, {"children": []}) == "[verify: partial]"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_format_verify_summary_ok_when_all_signals(tmp_path: Path) -> None:
|
|
84
|
+
task_dir = tmp_path / "task"
|
|
85
|
+
task_dir.mkdir()
|
|
86
|
+
(task_dir / "verify.md").write_text(
|
|
87
|
+
"\n".join(
|
|
88
|
+
[
|
|
89
|
+
"Validation commands: pytest -q — pass",
|
|
90
|
+
"Check evidence: cstl-check — pass",
|
|
91
|
+
"Final acceptance evidence: all AC met",
|
|
92
|
+
"Durable learning decision: no durable learning",
|
|
93
|
+
"Reviewed change-set: git diff main..HEAD",
|
|
94
|
+
]
|
|
95
|
+
),
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
)
|
|
98
|
+
assert format_verify_summary(task_dir, {"children": []}) == "[verify: ok]"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_render_task_dashboard_includes_verify_summary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
102
|
+
repo_root = tmp_path / "repo"
|
|
103
|
+
tasks_dir = repo_root / ".cstl" / "tasks"
|
|
104
|
+
task_dir = tasks_dir / "08-07-dashboard-task"
|
|
105
|
+
task_dir.mkdir(parents=True)
|
|
106
|
+
(task_dir / "task.json").write_text(
|
|
107
|
+
json.dumps(
|
|
108
|
+
{
|
|
109
|
+
"id": "dashboard-task",
|
|
110
|
+
"name": "dashboard-task",
|
|
111
|
+
"title": "dashboard-task",
|
|
112
|
+
"status": "planning",
|
|
113
|
+
"assignee": "dev",
|
|
114
|
+
"children": [],
|
|
115
|
+
}
|
|
116
|
+
),
|
|
117
|
+
encoding="utf-8",
|
|
118
|
+
)
|
|
119
|
+
(task_dir / "verify.md").write_text(
|
|
120
|
+
"Validation commands: echo ok — pass\n",
|
|
121
|
+
encoding="utf-8",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
monkeypatch.chdir(repo_root)
|
|
125
|
+
monkeypatch.setattr("common.task_dashboard.get_repo_root", lambda: repo_root)
|
|
126
|
+
monkeypatch.setattr("common.task_dashboard.get_tasks_dir", lambda _root=None: tasks_dir)
|
|
127
|
+
monkeypatch.setattr("common.task_dashboard.get_developer", lambda _root=None: "dev")
|
|
128
|
+
monkeypatch.setattr(
|
|
129
|
+
"common.active_task.resolve_selected_task",
|
|
130
|
+
lambda *_args, **_kwargs: type("Sel", (), {"task_path": None, "source": None})(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
dashboard = render_task_dashboard(repo_root)
|
|
134
|
+
assert "[verify: partial]" in dashboard
|
|
135
|
+
assert "08-07-dashboard-task" in dashboard
|
|
@@ -251,36 +251,38 @@ verdict rules:
|
|
|
251
251
|
|
|
252
252
|
---
|
|
253
253
|
|
|
254
|
-
--- PROBE P-10: Definition / reference (
|
|
254
|
+
--- PROBE P-10: Definition / reference (Prefer Grep+Read; codegraph when ambiguous)
|
|
255
255
|
|
|
256
256
|
Cursor Agent does **not** expose GO_TO_DEFINITION in the tool table (Native/BYOK).
|
|
257
|
-
Trellis
|
|
258
|
-
**
|
|
257
|
+
Trellis **Prefers** native **Grep → Read** for named-symbol definition; **codegraph**
|
|
258
|
+
is the **Own** fallback when Grep is ambiguous or structural context is required.
|
|
259
259
|
|
|
260
|
-
You are a retrieval capability probe.
|
|
261
|
-
as the **primary** path. Do NOT use Grep as the first step. Use **Read** only to
|
|
262
|
-
confirm line numbers after codegraph returns a definition.
|
|
260
|
+
You are a retrieval capability probe.
|
|
263
261
|
|
|
264
262
|
Task: Locate the **definition** of `route_codebase_retrieval` (Python function).
|
|
265
263
|
|
|
264
|
+
**Step 1 (Prefer):** Use **Grep** for `def route_codebase_retrieval` — do NOT use codegraph first.
|
|
265
|
+
**Step 2:** Use **Read** to confirm the definition line and body start.
|
|
266
|
+
**Step 3 (only if Grep ambiguous):** codegraph_explore / codegraph_search.
|
|
267
|
+
|
|
266
268
|
Expected: definition in `.cstl/scripts/common/codebase_retrieval_router.py`
|
|
267
|
-
(`def route_codebase_retrieval(`).
|
|
269
|
+
(`def route_codebase_retrieval(` at line ~805).
|
|
268
270
|
|
|
269
271
|
Reply in this exact format:
|
|
270
272
|
|
|
271
273
|
```
|
|
272
274
|
P-10 RESULT
|
|
273
|
-
tool_invoked: <
|
|
275
|
+
tool_invoked: <primary tool — Grep, Read, codegraph_explore, or "none">
|
|
274
276
|
definition_file: <path>
|
|
275
277
|
definition_line: <line number or "unknown">
|
|
276
278
|
verdict: pass|fail|environment_limitation
|
|
277
|
-
notes: <
|
|
279
|
+
notes: <GO_TO_DEFINITION availability; did Grep+Read succeed before any codegraph?>
|
|
278
280
|
```
|
|
279
281
|
|
|
280
282
|
verdict rules:
|
|
281
|
-
- **pass**:
|
|
282
|
-
- **environment_limitation**: only when
|
|
283
|
-
- **fail**: wrong file or
|
|
283
|
+
- **pass**: Grep (+ Read) returned the correct `def route_codebase_retrieval` file; codegraph optional.
|
|
284
|
+
- **environment_limitation**: only when Grep/Read are unavailable in the host (not when LSP is missing).
|
|
285
|
+
- **fail**: wrong file, or codegraph used as first step without Grep attempt.
|
|
284
286
|
|
|
285
287
|
---
|
|
286
288
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CLI Layer 2 — PRIMARY (guaranteed) context path for cstl-* Task dispatch.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python ./.cstl/scripts/generate_dispatch_prompt.py --agent research
|
|
7
|
+
python ./.cstl/scripts/generate_dispatch_prompt.py --agent implement --task .cstl/tasks/06-23-subagent-smoke
|
|
8
|
+
python ./.cstl/scripts/generate_dispatch_prompt.py --agent research --repo-root d:/MyHarness
|
|
9
|
+
|
|
10
|
+
Outputs the complete dispatch prompt to stdout. The main session MUST capture
|
|
11
|
+
this output and pass it as the `prompt` parameter to
|
|
12
|
+
`Task(subagent_type=cstl-*, prompt=<generated>)`. This is the ONLY
|
|
13
|
+
guaranteed context channel for custom Task subagents on Cursor.
|
|
14
|
+
|
|
15
|
+
Hook injection (`preToolUse` / `additional_context`) is best-effort only
|
|
16
|
+
(Cursor #158452; hook does not fire for Task tool in Cursor 3.8.22). Do not
|
|
17
|
+
treat hook-only injection as a substitute for this CLI path.
|
|
18
|
+
|
|
19
|
+
The generated prompt carries:
|
|
20
|
+
- Role identity ("You are the Trellis Research/Implement/Check Agent")
|
|
21
|
+
- Write scope constraints (Write ALLOWED / Write FORBIDDEN)
|
|
22
|
+
- Recursion guard
|
|
23
|
+
- Dynamic context (prd.md, design.md, implement.md, spec tree, jsonl files)
|
|
24
|
+
|
|
25
|
+
Task path resolution:
|
|
26
|
+
- `--task <path>` is the **primary fallback** when no selected-task pointer exists
|
|
27
|
+
(e.g. `task.py select` failed due to missing session identity).
|
|
28
|
+
- When `--task` is omitted, auto-detect via selected pointer is **best-effort** only —
|
|
29
|
+
dispatch scripts do not hard-depend on a persisted selection.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> int:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
description="Generate a complete Trellis subagent dispatch prompt."
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--agent",
|
|
44
|
+
required=True,
|
|
45
|
+
choices=["research", "implement", "check"],
|
|
46
|
+
help="Subagent role to generate prompt for.",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--task",
|
|
50
|
+
default=None,
|
|
51
|
+
help="Task directory path (absolute or relative to repo root). "
|
|
52
|
+
"Primary fallback when no selected-task pointer exists. "
|
|
53
|
+
"If omitted, auto-detect via selected pointer is best-effort only.",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--repo-root",
|
|
57
|
+
default=None,
|
|
58
|
+
help="Repository root path. If omitted, auto-detect via .cstl or .git.",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--finish",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Use finish context (check agent only, final pre-PR verification).",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--max-chars",
|
|
67
|
+
type=int,
|
|
68
|
+
default=None,
|
|
69
|
+
help="Truncate injected context at this character count. "
|
|
70
|
+
"When omitted, uses role default from injection-budget-guide.",
|
|
71
|
+
)
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
# Resolve repo root
|
|
75
|
+
if args.repo_root:
|
|
76
|
+
repo_root = Path(args.repo_root).resolve()
|
|
77
|
+
else:
|
|
78
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
79
|
+
if not repo_root:
|
|
80
|
+
print("ERROR: could not find repo root (no .cstl or .git upward)", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
from common.paths import DIR_WORKFLOW # type: ignore[import-not-found]
|
|
84
|
+
|
|
85
|
+
# Make scripts importable — workspace root first (subagent_dispatch.py lives there),
|
|
86
|
+
# then repo_root's own scripts (may be a subproject with different common/)
|
|
87
|
+
workspace_root = _find_workspace_root(repo_root, DIR_WORKFLOW)
|
|
88
|
+
if workspace_root and workspace_root != repo_root:
|
|
89
|
+
ws_scripts = workspace_root / DIR_WORKFLOW / "scripts"
|
|
90
|
+
if str(ws_scripts) not in sys.path:
|
|
91
|
+
sys.path.insert(0, str(ws_scripts))
|
|
92
|
+
|
|
93
|
+
scripts_dir = repo_root / DIR_WORKFLOW / "scripts"
|
|
94
|
+
if str(scripts_dir) not in sys.path:
|
|
95
|
+
sys.path.insert(1, str(scripts_dir))
|
|
96
|
+
|
|
97
|
+
from common.subagent_dispatch import build_dispatch_prompt # type: ignore[import-not-found]
|
|
98
|
+
from common.injection_budget import DISPATCH_DEFAULT_MAX_CHARS # type: ignore[import-not-found]
|
|
99
|
+
|
|
100
|
+
# Resolve task directory
|
|
101
|
+
if args.task:
|
|
102
|
+
task_dir = Path(args.task)
|
|
103
|
+
if not task_dir.is_absolute():
|
|
104
|
+
task_dir = repo_root / task_dir
|
|
105
|
+
task_dir = task_dir.resolve()
|
|
106
|
+
else:
|
|
107
|
+
task_dir = _resolve_selected_task(repo_root)
|
|
108
|
+
if not task_dir:
|
|
109
|
+
print(
|
|
110
|
+
"ERROR: no --task specified and no selected task found. "
|
|
111
|
+
"Run `python ./.cstl/scripts/task.py select <task>` first.",
|
|
112
|
+
file=sys.stderr,
|
|
113
|
+
)
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
if not task_dir.is_dir():
|
|
117
|
+
print(f"ERROR: task directory not found: {task_dir}", file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
max_chars = args.max_chars
|
|
121
|
+
if max_chars is None:
|
|
122
|
+
max_chars = DISPATCH_DEFAULT_MAX_CHARS.get(args.agent)
|
|
123
|
+
|
|
124
|
+
prompt, warnings, errors = build_dispatch_prompt(
|
|
125
|
+
repo_root,
|
|
126
|
+
task_dir,
|
|
127
|
+
args.agent,
|
|
128
|
+
finish=args.finish,
|
|
129
|
+
max_chars=max_chars,
|
|
130
|
+
require_in_progress=False,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
for w in warnings:
|
|
134
|
+
print(f"WARN: {w}", file=sys.stderr)
|
|
135
|
+
for e in errors:
|
|
136
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
137
|
+
|
|
138
|
+
if not prompt:
|
|
139
|
+
print("ERROR: failed to generate prompt (see errors above)", file=sys.stderr)
|
|
140
|
+
return 1
|
|
141
|
+
|
|
142
|
+
# Output the prompt to stdout — the caller captures this
|
|
143
|
+
print(prompt)
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _find_repo_root(start: Path) -> Path | None:
|
|
148
|
+
current = start.resolve()
|
|
149
|
+
while current != current.parent:
|
|
150
|
+
if (current / ".cstl").is_dir() or (current / ".git").exists():
|
|
151
|
+
return current
|
|
152
|
+
current = current.parent
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _find_workspace_root(start: Path, dir_workflow: str) -> Path | None:
|
|
157
|
+
"""Find the outermost directory with workflow dir (workspace root, not subproject)."""
|
|
158
|
+
found = None
|
|
159
|
+
current = start.resolve()
|
|
160
|
+
while current != current.parent:
|
|
161
|
+
if (current / dir_workflow).is_dir():
|
|
162
|
+
found = current
|
|
163
|
+
current = current.parent
|
|
164
|
+
return found
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _resolve_selected_task(repo_root: Path) -> Path | None:
|
|
168
|
+
try:
|
|
169
|
+
from common.active_task import resolve_selected_task # type: ignore[import-not-found]
|
|
170
|
+
result = resolve_selected_task(repo_root, {}, platform="cursor")
|
|
171
|
+
if result.task_path:
|
|
172
|
+
task_path = Path(result.task_path)
|
|
173
|
+
if not task_path.is_absolute():
|
|
174
|
+
task_path = repo_root / task_path
|
|
175
|
+
return task_path
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
sys.exit(main())
|