@blxzer/cursor-trellis 0.2.9 → 0.3.2
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 +94 -0
- package/README.md +85 -19
- package/README.zh-CN.md +60 -21
- package/bin/smart-search.js +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +46 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/capability-smoke.d.ts +24 -0
- package/dist/commands/capability-smoke.d.ts.map +1 -0
- package/dist/commands/capability-smoke.js +83 -0
- package/dist/commands/capability-smoke.js.map +1 -0
- package/dist/commands/channel/agent-loader.d.ts +1 -1
- package/dist/commands/channel/agent-loader.js +4 -4
- package/dist/commands/channel/agent-loader.js.map +1 -1
- package/dist/commands/channel/guard.d.ts +2 -2
- package/dist/commands/channel/guard.js +3 -3
- package/dist/commands/channel/guard.js.map +1 -1
- package/dist/commands/channel/index.js +3 -3
- package/dist/commands/channel/index.js.map +1 -1
- package/dist/commands/channel/list.d.ts +1 -1
- package/dist/commands/channel/list.js +1 -1
- package/dist/commands/channel/rm.d.ts +1 -1
- package/dist/commands/channel/rm.js +1 -1
- package/dist/commands/channel/store/lock.d.ts +1 -1
- package/dist/commands/channel/store/lock.js +1 -1
- package/dist/commands/channel/store/paths.js +1 -1
- package/dist/commands/channel/store/paths.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +197 -91
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/uninstall.d.ts +4 -4
- package/dist/commands/uninstall.js +12 -12
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts +7 -2
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +161 -61
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/upgrade.js +2 -2
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/workflow.d.ts +4 -4
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +10 -9
- package/dist/commands/workflow.js.map +1 -1
- package/dist/configurators/cursor.d.ts +2 -2
- package/dist/configurators/cursor.js +4 -4
- package/dist/configurators/cursor.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +4 -4
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/shared.d.ts +10 -10
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +32 -32
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts +6 -6
- package/dist/configurators/workflow.js +1 -1
- package/dist/constants/paths.d.ts +24 -24
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +16 -16
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.2.10.json +374 -0
- package/dist/migrations/manifests/0.3.0.json +89 -0
- package/dist/migrations/manifests/0.3.1.json +32 -0
- package/dist/templates/common/bundled-skills/{trellis-cursor2plus-setup → cstl-cursor2plus-setup}/SKILL.md +15 -14
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/SKILL.md +75 -75
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/add-project-local-conventions.md +19 -19
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-agents.md +8 -8
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-context-loading.md +26 -26
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-hooks.md +9 -9
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-skills-or-commands.md +8 -8
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-spec-structure.md +17 -17
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-task-lifecycle.md +46 -46
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-workflow.md +18 -18
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/overview.md +55 -55
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/context-injection.md +20 -20
- package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/generated-files.md +83 -0
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/overview.md +15 -15
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/spec-system.md +12 -12
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/task-system.md +34 -34
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/workflow.md +8 -8
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/workspace-memory.md +19 -19
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/agents.md +12 -12
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/hooks-and-settings.md +7 -7
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/overview.md +9 -9
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/platform-map.md +7 -7
- package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/skills-and-commands.md +19 -19
- package/dist/templates/common/bundled-skills/{trellis-micro-grill → cstl-micro-grill}/SKILL.md +62 -47
- package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/SKILL.md +44 -44
- package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/references/authoring-rules.md +97 -97
- package/dist/templates/common/bundled-skills/{trellis-skill-creator/references/trellis-skill-locations.md → cstl-skill-creator/references/cstl-skill-locations.md} +52 -52
- package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/references/review-checklist.md +47 -47
- package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/SKILL.md +6 -6
- package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/repository-analysis.md +1 -1
- package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/spec-task-planning.md +3 -3
- package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/spec-writing.md +1 -1
- package/dist/templates/common/commands/continue.md +24 -24
- package/dist/templates/common/commands/finish-work.md +5 -5
- package/dist/templates/common/commands/start.md +20 -20
- package/dist/templates/common/index.d.ts +1 -1
- package/dist/templates/common/index.d.ts.map +1 -1
- package/dist/templates/common/skills/before-dev.md +4 -4
- package/dist/templates/common/skills/brainstorm.md +188 -164
- package/dist/templates/common/skills/break-loop.md +3 -3
- package/dist/templates/common/skills/check.md +5 -5
- package/dist/templates/common/skills/update-spec.md +4 -4
- package/dist/templates/cursor/agents/{trellis-check.md → cstl-check.md} +14 -14
- package/dist/templates/cursor/agents/{trellis-implement.md → cstl-implement.md} +14 -14
- package/dist/templates/cursor/agents/{trellis-research.md → cstl-research.md} +11 -11
- package/dist/templates/cursor/commands/cursor2plus-setup.md +15 -15
- package/dist/templates/cursor/fixtures/expected-rules.d.ts +1 -1
- package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
- package/dist/templates/cursor/fixtures/expected-rules.js +2 -2
- package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
- package/dist/templates/cursor/rules/{trellis-subagent-dispatch.mdc → cstl-subagent-dispatch.mdc} +4 -4
- package/dist/templates/cursor/rules/{trellis-triage.mdc → cstl-triage.mdc} +3 -3
- package/dist/templates/cursor/rules/retrieval-routing.mdc +4 -4
- package/dist/templates/extract.d.ts +1 -1
- package/dist/templates/extract.js +1 -1
- package/dist/templates/markdown/agents.md +13 -13
- package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +1 -1
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +634 -634
- package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +5 -5
- package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +7 -1
- package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +393 -391
- package/dist/templates/markdown/spec/guides/durable-learning-decision-guide.md.txt +14 -14
- package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +5 -5
- package/dist/templates/markdown/spec/guides/index.md.txt +3 -2
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +10 -10
- package/dist/templates/markdown/workspace-index.md +3 -3
- package/dist/templates/markdown/worktree.yaml.txt +1 -1
- package/dist/templates/shared-hooks/index.d.ts +2 -2
- package/dist/templates/shared-hooks/index.js +2 -2
- package/dist/templates/shared-hooks/inject-retrieval-plan.py +3 -3
- package/dist/templates/shared-hooks/inject-shell-session-context.py +7 -7
- package/dist/templates/shared-hooks/inject-subagent-context.py +4 -4
- package/dist/templates/shared-hooks/inject-workflow-state.py +37 -37
- package/dist/templates/shared-hooks/research-end-retrieval-pack.py +2 -2
- package/dist/templates/shared-hooks/session-start.py +16 -16
- package/dist/templates/trellis/config/execution-strategy-rules.json +1 -1
- package/dist/templates/trellis/config.yaml +5 -25
- package/dist/templates/trellis/index.d.ts +2 -2
- package/dist/templates/trellis/index.js +2 -2
- package/dist/templates/trellis/local/README.md +15 -13
- package/dist/templates/trellis/local/patch_wpelc8.py +7 -7
- package/dist/templates/trellis/local/subagent-models.json.example +1 -1
- package/dist/templates/trellis/local/trellis-task-models.json.example +3 -3
- package/dist/templates/trellis/local/trellis-task-models.json5.example +5 -5
- package/dist/templates/trellis/local/trellis_task_models_config.py +6 -6
- package/dist/templates/trellis/scripts/add_session.py +20 -20
- package/dist/templates/trellis/scripts/common/active_task.py +2 -2
- package/dist/templates/trellis/scripts/common/artifact_search.py +587 -587
- package/dist/templates/trellis/scripts/common/cli_adapter.py +3 -3
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +6 -6
- package/dist/templates/trellis/scripts/common/config.py +3 -3
- package/dist/templates/trellis/scripts/common/developer.py +2 -2
- package/dist/templates/trellis/scripts/common/execution_strategy.py +1 -1
- package/dist/templates/trellis/scripts/common/packages_context.py +23 -23
- package/dist/templates/trellis/scripts/common/parent_orchestration.py +702 -702
- package/dist/templates/trellis/scripts/common/paths.py +107 -107
- package/dist/templates/trellis/scripts/common/project_file_stats.py +1 -1
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +1 -1
- package/dist/templates/trellis/scripts/common/safe_commit.py +31 -31
- package/dist/templates/trellis/scripts/common/session_context.py +1159 -1159
- package/dist/templates/trellis/scripts/common/session_memory.py +360 -360
- package/dist/templates/trellis/scripts/common/smart_search_evidence.py +514 -514
- package/dist/templates/trellis/scripts/common/smart_search_resolve.py +1 -1
- package/dist/templates/trellis/scripts/common/subagent_dispatch.py +7 -7
- package/dist/templates/trellis/scripts/common/task_context.py +1 -1
- package/dist/templates/trellis/scripts/common/task_dashboard.py +140 -140
- package/dist/templates/trellis/scripts/common/task_gates.py +2056 -2034
- package/dist/templates/trellis/scripts/common/task_store.py +1594 -1594
- package/dist/templates/trellis/scripts/common/task_utils.py +3 -3
- package/dist/templates/trellis/scripts/common/tasks.py +1 -1
- package/dist/templates/trellis/scripts/common/trellis_config.py +3 -3
- package/dist/templates/trellis/scripts/common/workflow_phase.py +1 -1
- package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +3 -3
- package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +11 -11
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +8 -8
- package/dist/templates/trellis/scripts/init_developer.py +2 -2
- package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +4 -4
- package/dist/templates/trellis/scripts/route_codebase_retrieval.py +1 -1
- package/dist/templates/trellis/scripts/task.py +918 -918
- package/dist/templates/trellis/tasks/templates/release-readiness/implement.md +2 -2
- package/dist/templates/trellis/workflow.md +805 -805
- package/dist/types/ai-tools.d.ts +1 -1
- package/dist/types/ai-tools.d.ts.map +1 -1
- package/dist/types/ai-tools.js +1 -1
- package/dist/types/ai-tools.js.map +1 -1
- package/dist/types/migration.d.ts +1 -1
- package/dist/types/migration.d.ts.map +1 -1
- package/dist/utils/agents-md.d.ts +37 -0
- package/dist/utils/agents-md.d.ts.map +1 -0
- package/dist/utils/agents-md.js +82 -0
- package/dist/utils/agents-md.js.map +1 -0
- package/dist/utils/codebase-retrieval-router.js +5 -5
- package/dist/utils/codebase-retrieval-router.js.map +1 -1
- package/dist/utils/manifest-prune.d.ts +2 -2
- package/dist/utils/manifest-prune.js +4 -4
- package/dist/utils/manifest-prune.js.map +1 -1
- package/dist/utils/mirror-check.d.ts +3 -1
- package/dist/utils/mirror-check.d.ts.map +1 -1
- package/dist/utils/mirror-check.js +22 -11
- package/dist/utils/mirror-check.js.map +1 -1
- package/dist/utils/project-capabilities.d.ts +14 -3
- package/dist/utils/project-capabilities.d.ts.map +1 -1
- package/dist/utils/project-capabilities.js +127 -17
- package/dist/utils/project-capabilities.js.map +1 -1
- package/dist/utils/readiness.d.ts +10 -0
- package/dist/utils/readiness.d.ts.map +1 -1
- package/dist/utils/readiness.js +6 -3
- package/dist/utils/readiness.js.map +1 -1
- package/dist/utils/retrieval-agent-instructions.js +1 -1
- package/dist/utils/retrieval-agent-instructions.js.map +1 -1
- package/dist/utils/retrieval-execution-telemetry.js +1 -1
- package/dist/utils/retrieval-execution-telemetry.js.map +1 -1
- package/dist/utils/template-fetcher.js +1 -1
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts +2 -2
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +12 -10
- package/dist/utils/template-hash.js.map +1 -1
- package/dist/utils/workflow-dir.d.ts +13 -0
- package/dist/utils/workflow-dir.d.ts.map +1 -0
- package/dist/utils/workflow-dir.js +35 -0
- package/dist/utils/workflow-dir.js.map +1 -0
- package/dist/utils/workflow-ownership.d.ts +36 -0
- package/dist/utils/workflow-ownership.d.ts.map +1 -0
- package/dist/utils/workflow-ownership.js +137 -0
- package/dist/utils/workflow-ownership.js.map +1 -0
- package/dist/utils/workflow-resolver.d.ts +1 -1
- package/dist/utils/workflow-resolver.js +1 -1
- package/package.json +3 -4
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +0 -83
- /package/bin/{trellis.js → cstl.js} +0 -0
- /package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/mcp-setup.md +0 -0
|
@@ -1,2034 +1,2056 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Task quality gate helpers.
|
|
4
|
-
|
|
5
|
-
Gate records are stored in task.json under:
|
|
6
|
-
quality_gate_results.transitions[transition][gate]
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import hashlib
|
|
12
|
-
import json
|
|
13
|
-
import re
|
|
14
|
-
from dataclasses import dataclass, field
|
|
15
|
-
from datetime import datetime, timezone
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
from .task_map import (
|
|
19
|
-
get_child_state,
|
|
20
|
-
load_task_map,
|
|
21
|
-
validate_child_archive_state,
|
|
22
|
-
validate_parent_children_complete,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
SCHEMA_VERSION = 1
|
|
26
|
-
BASELINE_GATE = "baseline-check"
|
|
27
|
-
|
|
28
|
-
KNOWN_TRANSITIONS = {
|
|
29
|
-
"start-execution",
|
|
30
|
-
"full-task-complete",
|
|
31
|
-
"child-review",
|
|
32
|
-
"parent-changes",
|
|
33
|
-
"parent-accepted",
|
|
34
|
-
"parent-integrating",
|
|
35
|
-
"parent-integrated",
|
|
36
|
-
"parent-cancelled",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
REVIEW_GATES = {
|
|
40
|
-
"requirements-review",
|
|
41
|
-
"code-review",
|
|
42
|
-
"architecture-review",
|
|
43
|
-
"architecture-deep-review",
|
|
44
|
-
"integration-review",
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
KNOWN_GATES = {BASELINE_GATE, *REVIEW_GATES}
|
|
48
|
-
RESULTS = {"PASS", "FAIL", "SKIPPED"}
|
|
49
|
-
FAIL_ROOT_CAUSES = {
|
|
50
|
-
"implementation-defect": "Execution",
|
|
51
|
-
"contract-changing-defect": "Planning",
|
|
52
|
-
"validation-environment-blocker": "Verification / Review",
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
PROFILE_DEFAULT_GATES = {
|
|
56
|
-
"standard": ["requirements-review", "code-review"],
|
|
57
|
-
"strict": ["requirements-review", "code-review"],
|
|
58
|
-
"architecture": [
|
|
59
|
-
"requirements-review",
|
|
60
|
-
"architecture-review",
|
|
61
|
-
"code-review",
|
|
62
|
-
],
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
ALLOWED_EXECUTION_MODES = {"inline", "worker", "child-task"}
|
|
66
|
-
ALLOWED_ISOLATION = {"main-worktree", "git-worktree"}
|
|
67
|
-
ALLOWED_VERIFICATION_PROFILES = {"standard", "strict", "architecture"}
|
|
68
|
-
ALLOWED_RETRIEVAL_PROFILES = {
|
|
69
|
-
"exact-only",
|
|
70
|
-
"semantic",
|
|
71
|
-
"structure",
|
|
72
|
-
"architecture-memory",
|
|
73
|
-
}
|
|
74
|
-
ALLOWED_GATE_MODES = {"profile", "explicit"}
|
|
75
|
-
|
|
76
|
-
MAX_SHORT_FIELD = 240
|
|
77
|
-
MAX_REASON = 500
|
|
78
|
-
REVIEWER_RE = re.compile(r"^[A-Za-z0-9_.:@/-]+$")
|
|
79
|
-
VALIDATION_EVIDENCE_RE = re.compile(
|
|
80
|
-
r"(?im)^\s*(?:[-*]\s*)?validation"
|
|
81
|
-
r"(?:\s+(?:commands?|results?|evidence))?\s*:\s*(\S[^\r\n]*)$"
|
|
82
|
-
)
|
|
83
|
-
ACCEPTANCE_EVIDENCE_RE = re.compile(
|
|
84
|
-
r"(?im)^\s*(?:[-*]\s*)?(?:final\s+|user\s+)?acceptance"
|
|
85
|
-
r"(?:\s+evidence)?\s*:\s*(\S[^\r\n]*)$"
|
|
86
|
-
)
|
|
87
|
-
ACCEPTED_BY_USER_RE = re.compile(
|
|
88
|
-
r"(?im)^\s*(?:[-*]\s*)?accepted\s+by\s+user\s*:\s*(\S[^\r\n]*)$"
|
|
89
|
-
)
|
|
90
|
-
NO_DURABLE_LEARNING_RE = re.compile(r"(?i)\bno\s+durable\s+learning\b")
|
|
91
|
-
DURABLE_LEARNING_EVIDENCE_RE = re.compile(
|
|
92
|
-
r"(?im)^\s*(?:[-*]\s*)?(?:durable\s+learning(?:\s+decision)?|learning\s+decision|"
|
|
93
|
-
r"spec\s+updates?|spec\s+update\s+(?:needed|evidence)|updated\s+spec|"
|
|
94
|
-
r"retrospective(?:\.md)?|learning\s+artifact)\s*:\s*(\S[^\r\n]*)$"
|
|
95
|
-
)
|
|
96
|
-
INTEGRATION_EVIDENCE_RE = re.compile(
|
|
97
|
-
r"(?im)^\s*(?:[-*]\s*)?(?:final\s+)?integration"
|
|
98
|
-
r"(?:\s+evidence)?\s*:\s*(\S[^\r\n]*)$"
|
|
99
|
-
)
|
|
100
|
-
REVIEWED_CHANGE_SET_RE = re.compile(
|
|
101
|
-
r"(?im)^\s*(?:[-*]\s*)?(?:reviewed\s+)?"
|
|
102
|
-
r"(?:change[- ]set|changeset|diff|git\s+diff|ref|git\s+ref)"
|
|
103
|
-
r"(?:\s+(?:identity|evidence|summary|ref))?\s*:\s*(\S[^\r\n]*)$"
|
|
104
|
-
)
|
|
105
|
-
CHECK_EVIDENCE_RE = re.compile(
|
|
106
|
-
r"(?im)^\s*(?:[-*]\s*)?(?:check\s+evidence|
|
|
107
|
-
r"\s*:\s*(\S[^\r\n]*)$"
|
|
108
|
-
)
|
|
109
|
-
PLACEHOLDER_VALUES_RE = re.compile(
|
|
110
|
-
r"(?i)^(TBD|TODO|待定|待补充|N/?A|NA|NONE|-|\.\.\.)$"
|
|
111
|
-
)
|
|
112
|
-
PRD_ACCEPTANCE_CRITERIA_HEADING_RE = re.compile(
|
|
113
|
-
r"(?im)^\s*#{1,6}\s*acceptance\s+criteria\s*$"
|
|
114
|
-
)
|
|
115
|
-
PRD_ACCEPTANCE_ITEM_RE = re.compile(
|
|
116
|
-
r"(?im)^\s*-\s*\[[ xX]\]\s+(.+)$"
|
|
117
|
-
)
|
|
118
|
-
PRD_PLACEHOLDER_RE = re.compile(r"(?i)^\s*(TBD|TODO|待定|待补充)(?:\s*[.::])?\s*$")
|
|
119
|
-
AUTO_PLANNING_REVIEWER = "
|
|
120
|
-
START_EXECUTION_AUTO_GATES = frozenset({"requirements-review", "architecture-review"})
|
|
121
|
-
|
|
122
|
-
STABLE_TASK_KEYS = (
|
|
123
|
-
"id",
|
|
124
|
-
"name",
|
|
125
|
-
"title",
|
|
126
|
-
"description",
|
|
127
|
-
"status",
|
|
128
|
-
"dev_type",
|
|
129
|
-
"scope",
|
|
130
|
-
"package",
|
|
131
|
-
"priority",
|
|
132
|
-
"creator",
|
|
133
|
-
"assignee",
|
|
134
|
-
"parent",
|
|
135
|
-
"children",
|
|
136
|
-
"subtasks",
|
|
137
|
-
"relatedFiles",
|
|
138
|
-
"notes",
|
|
139
|
-
"meta",
|
|
140
|
-
"branch",
|
|
141
|
-
"base_branch",
|
|
142
|
-
"task_kind",
|
|
143
|
-
"task_type",
|
|
144
|
-
"kind",
|
|
145
|
-
"mode",
|
|
146
|
-
"contract_epoch",
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
@dataclass
|
|
151
|
-
class GateGuardResult:
|
|
152
|
-
"""Result from a protected-transition guard."""
|
|
153
|
-
|
|
154
|
-
ok: bool
|
|
155
|
-
errors: list[str] = field(default_factory=list)
|
|
156
|
-
warnings: list[str] = field(default_factory=list)
|
|
157
|
-
contract_fingerprint: str = ""
|
|
158
|
-
artifact_fingerprints: dict[str, str] = field(default_factory=dict)
|
|
159
|
-
required_gates: list[str] = field(default_factory=list)
|
|
160
|
-
baseline_record: dict | None = None
|
|
161
|
-
is_full_task: bool = False
|
|
162
|
-
closeout_profile: str = "lite"
|
|
163
|
-
auto_gate_records: dict[str, dict] = field(default_factory=dict)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def utc_now() -> str:
|
|
167
|
-
"""Return a compact UTC timestamp for task.json records."""
|
|
168
|
-
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
|
|
169
|
-
"+00:00", "Z"
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def normalize_result(result: str) -> str:
|
|
174
|
-
"""Normalize a CLI result value."""
|
|
175
|
-
return result.strip().upper()
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def task_closeout_profile(task_dir: Path, task_data: dict | None = None) -> str:
|
|
179
|
-
"""Return lite, full, or parent closeout profile."""
|
|
180
|
-
data = task_data or {}
|
|
181
|
-
child_names = data.get("children")
|
|
182
|
-
if isinstance(child_names, list) and any(isinstance(name, str) for name in child_names):
|
|
183
|
-
return "parent"
|
|
184
|
-
|
|
185
|
-
meta = data.get("meta") if isinstance(data.get("meta"), dict) else {}
|
|
186
|
-
candidates = [
|
|
187
|
-
data.get("task_kind"),
|
|
188
|
-
data.get("task_type"),
|
|
189
|
-
data.get("kind"),
|
|
190
|
-
data.get("mode"),
|
|
191
|
-
meta.get("task_kind"),
|
|
192
|
-
meta.get("task_type"),
|
|
193
|
-
meta.get("classification"),
|
|
194
|
-
meta.get("mode"),
|
|
195
|
-
]
|
|
196
|
-
|
|
197
|
-
explicit_lite = False
|
|
198
|
-
for value in candidates:
|
|
199
|
-
if not isinstance(value, str):
|
|
200
|
-
continue
|
|
201
|
-
normalized = value.lower().replace("_", "-")
|
|
202
|
-
if "parent" in normalized:
|
|
203
|
-
return "parent"
|
|
204
|
-
if "full" in normalized:
|
|
205
|
-
return "full"
|
|
206
|
-
if "lite" in normalized:
|
|
207
|
-
explicit_lite = True
|
|
208
|
-
|
|
209
|
-
if explicit_lite:
|
|
210
|
-
return "lite"
|
|
211
|
-
|
|
212
|
-
if (task_dir / "design.md").is_file() and (task_dir / "implement.md").is_file():
|
|
213
|
-
return "full"
|
|
214
|
-
return "lite"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def is_full_task(task_dir: Path, task_data: dict | None = None) -> bool:
|
|
218
|
-
"""Return True when a task should satisfy Full Task gates."""
|
|
219
|
-
return task_closeout_profile(task_dir, task_data) == "full"
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
gates
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if not isinstance(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def
|
|
364
|
-
"""Return non-baseline gates
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
"
|
|
398
|
-
|
|
399
|
-
return
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
def
|
|
403
|
-
task_dir: Path,
|
|
404
|
-
task_data: dict,
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
"
|
|
412
|
-
"
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
task_dir,
|
|
441
|
-
|
|
442
|
-
return
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
errors
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if transition == "
|
|
773
|
-
if profile != "full":
|
|
774
|
-
return errors
|
|
775
|
-
contract, contract_errors = read_strategy_contract(task_dir)
|
|
776
|
-
errors.extend(contract_errors)
|
|
777
|
-
if contract_errors:
|
|
778
|
-
return errors
|
|
779
|
-
return errors + _complete_transition_gate_errors(
|
|
780
|
-
task_dir,
|
|
781
|
-
task_data,
|
|
782
|
-
transition,
|
|
783
|
-
contract,
|
|
784
|
-
required_gates_for_transition(transition, contract),
|
|
785
|
-
)
|
|
786
|
-
|
|
787
|
-
if transition == "
|
|
788
|
-
if profile != "
|
|
789
|
-
return errors
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
)
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
errors
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
)
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
"
|
|
873
|
-
"
|
|
874
|
-
|
|
875
|
-
"
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
)
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
)
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
if not
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
"
|
|
1176
|
-
"",
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
"
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
"
|
|
1277
|
-
"
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
"
|
|
1284
|
-
f"
|
|
1285
|
-
)
|
|
1286
|
-
continue
|
|
1287
|
-
if error.
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
hints.append(
|
|
1345
|
-
"
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
)
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
"",
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
or
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
messages
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
if
|
|
1559
|
-
messages.append(
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
if
|
|
1618
|
-
errors.append("
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
if
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
)
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
if not
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
"
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
)
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
"gate"
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
if
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
return
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
return
|
|
2011
|
-
if
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
if
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
return
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
def
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Task quality gate helpers.
|
|
4
|
+
|
|
5
|
+
Gate records are stored in task.json under:
|
|
6
|
+
quality_gate_results.transitions[transition][gate]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .task_map import (
|
|
19
|
+
get_child_state,
|
|
20
|
+
load_task_map,
|
|
21
|
+
validate_child_archive_state,
|
|
22
|
+
validate_parent_children_complete,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
SCHEMA_VERSION = 1
|
|
26
|
+
BASELINE_GATE = "baseline-check"
|
|
27
|
+
|
|
28
|
+
KNOWN_TRANSITIONS = {
|
|
29
|
+
"start-execution",
|
|
30
|
+
"full-task-complete",
|
|
31
|
+
"child-review",
|
|
32
|
+
"parent-changes",
|
|
33
|
+
"parent-accepted",
|
|
34
|
+
"parent-integrating",
|
|
35
|
+
"parent-integrated",
|
|
36
|
+
"parent-cancelled",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
REVIEW_GATES = {
|
|
40
|
+
"requirements-review",
|
|
41
|
+
"code-review",
|
|
42
|
+
"architecture-review",
|
|
43
|
+
"architecture-deep-review",
|
|
44
|
+
"integration-review",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
KNOWN_GATES = {BASELINE_GATE, *REVIEW_GATES}
|
|
48
|
+
RESULTS = {"PASS", "FAIL", "SKIPPED"}
|
|
49
|
+
FAIL_ROOT_CAUSES = {
|
|
50
|
+
"implementation-defect": "Execution",
|
|
51
|
+
"contract-changing-defect": "Planning",
|
|
52
|
+
"validation-environment-blocker": "Verification / Review",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
PROFILE_DEFAULT_GATES = {
|
|
56
|
+
"standard": ["requirements-review", "code-review"],
|
|
57
|
+
"strict": ["requirements-review", "code-review"],
|
|
58
|
+
"architecture": [
|
|
59
|
+
"requirements-review",
|
|
60
|
+
"architecture-review",
|
|
61
|
+
"code-review",
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ALLOWED_EXECUTION_MODES = {"inline", "worker", "child-task"}
|
|
66
|
+
ALLOWED_ISOLATION = {"main-worktree", "git-worktree"}
|
|
67
|
+
ALLOWED_VERIFICATION_PROFILES = {"standard", "strict", "architecture"}
|
|
68
|
+
ALLOWED_RETRIEVAL_PROFILES = {
|
|
69
|
+
"exact-only",
|
|
70
|
+
"semantic",
|
|
71
|
+
"structure",
|
|
72
|
+
"architecture-memory",
|
|
73
|
+
}
|
|
74
|
+
ALLOWED_GATE_MODES = {"profile", "explicit"}
|
|
75
|
+
|
|
76
|
+
MAX_SHORT_FIELD = 240
|
|
77
|
+
MAX_REASON = 500
|
|
78
|
+
REVIEWER_RE = re.compile(r"^[A-Za-z0-9_.:@/-]+$")
|
|
79
|
+
VALIDATION_EVIDENCE_RE = re.compile(
|
|
80
|
+
r"(?im)^\s*(?:[-*]\s*)?validation"
|
|
81
|
+
r"(?:\s+(?:commands?|results?|evidence))?\s*:\s*(\S[^\r\n]*)$"
|
|
82
|
+
)
|
|
83
|
+
ACCEPTANCE_EVIDENCE_RE = re.compile(
|
|
84
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:final\s+|user\s+)?acceptance"
|
|
85
|
+
r"(?:\s+evidence)?\s*:\s*(\S[^\r\n]*)$"
|
|
86
|
+
)
|
|
87
|
+
ACCEPTED_BY_USER_RE = re.compile(
|
|
88
|
+
r"(?im)^\s*(?:[-*]\s*)?accepted\s+by\s+user\s*:\s*(\S[^\r\n]*)$"
|
|
89
|
+
)
|
|
90
|
+
NO_DURABLE_LEARNING_RE = re.compile(r"(?i)\bno\s+durable\s+learning\b")
|
|
91
|
+
DURABLE_LEARNING_EVIDENCE_RE = re.compile(
|
|
92
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:durable\s+learning(?:\s+decision)?|learning\s+decision|"
|
|
93
|
+
r"spec\s+updates?|spec\s+update\s+(?:needed|evidence)|updated\s+spec|"
|
|
94
|
+
r"retrospective(?:\.md)?|learning\s+artifact)\s*:\s*(\S[^\r\n]*)$"
|
|
95
|
+
)
|
|
96
|
+
INTEGRATION_EVIDENCE_RE = re.compile(
|
|
97
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:final\s+)?integration"
|
|
98
|
+
r"(?:\s+evidence)?\s*:\s*(\S[^\r\n]*)$"
|
|
99
|
+
)
|
|
100
|
+
REVIEWED_CHANGE_SET_RE = re.compile(
|
|
101
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:reviewed\s+)?"
|
|
102
|
+
r"(?:change[- ]set|changeset|diff|git\s+diff|ref|git\s+ref)"
|
|
103
|
+
r"(?:\s+(?:identity|evidence|summary|ref))?\s*:\s*(\S[^\r\n]*)$"
|
|
104
|
+
)
|
|
105
|
+
CHECK_EVIDENCE_RE = re.compile(
|
|
106
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:check\s+evidence|cstl-check(?:\s+evidence)?)"
|
|
107
|
+
r"\s*:\s*(\S[^\r\n]*)$"
|
|
108
|
+
)
|
|
109
|
+
PLACEHOLDER_VALUES_RE = re.compile(
|
|
110
|
+
r"(?i)^(TBD|TODO|待定|待补充|N/?A|NA|NONE|-|\.\.\.)$"
|
|
111
|
+
)
|
|
112
|
+
PRD_ACCEPTANCE_CRITERIA_HEADING_RE = re.compile(
|
|
113
|
+
r"(?im)^\s*#{1,6}\s*acceptance\s+criteria\s*$"
|
|
114
|
+
)
|
|
115
|
+
PRD_ACCEPTANCE_ITEM_RE = re.compile(
|
|
116
|
+
r"(?im)^\s*-\s*\[[ xX]\]\s+(.+)$"
|
|
117
|
+
)
|
|
118
|
+
PRD_PLACEHOLDER_RE = re.compile(r"(?i)^\s*(TBD|TODO|待定|待补充)(?:\s*[.::])?\s*$")
|
|
119
|
+
AUTO_PLANNING_REVIEWER = "cstl-cli"
|
|
120
|
+
START_EXECUTION_AUTO_GATES = frozenset({"requirements-review", "architecture-review"})
|
|
121
|
+
|
|
122
|
+
STABLE_TASK_KEYS = (
|
|
123
|
+
"id",
|
|
124
|
+
"name",
|
|
125
|
+
"title",
|
|
126
|
+
"description",
|
|
127
|
+
"status",
|
|
128
|
+
"dev_type",
|
|
129
|
+
"scope",
|
|
130
|
+
"package",
|
|
131
|
+
"priority",
|
|
132
|
+
"creator",
|
|
133
|
+
"assignee",
|
|
134
|
+
"parent",
|
|
135
|
+
"children",
|
|
136
|
+
"subtasks",
|
|
137
|
+
"relatedFiles",
|
|
138
|
+
"notes",
|
|
139
|
+
"meta",
|
|
140
|
+
"branch",
|
|
141
|
+
"base_branch",
|
|
142
|
+
"task_kind",
|
|
143
|
+
"task_type",
|
|
144
|
+
"kind",
|
|
145
|
+
"mode",
|
|
146
|
+
"contract_epoch",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class GateGuardResult:
|
|
152
|
+
"""Result from a protected-transition guard."""
|
|
153
|
+
|
|
154
|
+
ok: bool
|
|
155
|
+
errors: list[str] = field(default_factory=list)
|
|
156
|
+
warnings: list[str] = field(default_factory=list)
|
|
157
|
+
contract_fingerprint: str = ""
|
|
158
|
+
artifact_fingerprints: dict[str, str] = field(default_factory=dict)
|
|
159
|
+
required_gates: list[str] = field(default_factory=list)
|
|
160
|
+
baseline_record: dict | None = None
|
|
161
|
+
is_full_task: bool = False
|
|
162
|
+
closeout_profile: str = "lite"
|
|
163
|
+
auto_gate_records: dict[str, dict] = field(default_factory=dict)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def utc_now() -> str:
|
|
167
|
+
"""Return a compact UTC timestamp for task.json records."""
|
|
168
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
|
|
169
|
+
"+00:00", "Z"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def normalize_result(result: str) -> str:
|
|
174
|
+
"""Normalize a CLI result value."""
|
|
175
|
+
return result.strip().upper()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def task_closeout_profile(task_dir: Path, task_data: dict | None = None) -> str:
|
|
179
|
+
"""Return lite, full, or parent closeout profile."""
|
|
180
|
+
data = task_data or {}
|
|
181
|
+
child_names = data.get("children")
|
|
182
|
+
if isinstance(child_names, list) and any(isinstance(name, str) for name in child_names):
|
|
183
|
+
return "parent"
|
|
184
|
+
|
|
185
|
+
meta = data.get("meta") if isinstance(data.get("meta"), dict) else {}
|
|
186
|
+
candidates = [
|
|
187
|
+
data.get("task_kind"),
|
|
188
|
+
data.get("task_type"),
|
|
189
|
+
data.get("kind"),
|
|
190
|
+
data.get("mode"),
|
|
191
|
+
meta.get("task_kind"),
|
|
192
|
+
meta.get("task_type"),
|
|
193
|
+
meta.get("classification"),
|
|
194
|
+
meta.get("mode"),
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
explicit_lite = False
|
|
198
|
+
for value in candidates:
|
|
199
|
+
if not isinstance(value, str):
|
|
200
|
+
continue
|
|
201
|
+
normalized = value.lower().replace("_", "-")
|
|
202
|
+
if "parent" in normalized:
|
|
203
|
+
return "parent"
|
|
204
|
+
if "full" in normalized:
|
|
205
|
+
return "full"
|
|
206
|
+
if "lite" in normalized:
|
|
207
|
+
explicit_lite = True
|
|
208
|
+
|
|
209
|
+
if explicit_lite:
|
|
210
|
+
return "lite"
|
|
211
|
+
|
|
212
|
+
if (task_dir / "design.md").is_file() and (task_dir / "implement.md").is_file():
|
|
213
|
+
return "full"
|
|
214
|
+
return "lite"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def is_full_task(task_dir: Path, task_data: dict | None = None) -> bool:
|
|
218
|
+
"""Return True when a task should satisfy Full Task gates."""
|
|
219
|
+
return task_closeout_profile(task_dir, task_data) == "full"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def resolve_strategy_contract_for_gates(
|
|
223
|
+
task_dir: Path,
|
|
224
|
+
task_data: dict | None,
|
|
225
|
+
) -> tuple[dict, list[str]]:
|
|
226
|
+
"""Return the Development Strategy Contract used for gate fingerprints."""
|
|
227
|
+
if task_data is None:
|
|
228
|
+
return {}, []
|
|
229
|
+
profile = task_closeout_profile(task_dir, task_data)
|
|
230
|
+
if profile == "full":
|
|
231
|
+
return read_strategy_contract(task_dir)
|
|
232
|
+
if profile == "parent" and (task_dir / "implement.md").is_file():
|
|
233
|
+
return read_strategy_contract(task_dir)
|
|
234
|
+
return {}, []
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def read_strategy_contract(task_dir: Path) -> tuple[dict, list[str]]:
|
|
238
|
+
"""Parse the lightweight Development Strategy Contract from implement.md."""
|
|
239
|
+
implement_path = task_dir / "implement.md"
|
|
240
|
+
if not implement_path.is_file():
|
|
241
|
+
return {}, ["implement.md"]
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
lines = implement_path.read_text(encoding="utf-8").splitlines()
|
|
245
|
+
except OSError:
|
|
246
|
+
return {}, ["implement.md could not be read"]
|
|
247
|
+
|
|
248
|
+
contract: dict = {}
|
|
249
|
+
section: str | None = None
|
|
250
|
+
list_key: str | None = None
|
|
251
|
+
started = False
|
|
252
|
+
|
|
253
|
+
top_level_keys = {
|
|
254
|
+
"execution_mode",
|
|
255
|
+
"isolation",
|
|
256
|
+
"verification_profile",
|
|
257
|
+
"retrieval_profile",
|
|
258
|
+
"optional_capabilities",
|
|
259
|
+
"quality_gates",
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for raw_line in lines:
|
|
263
|
+
line = raw_line.rstrip()
|
|
264
|
+
stripped = line.strip()
|
|
265
|
+
if not stripped:
|
|
266
|
+
continue
|
|
267
|
+
if stripped.startswith("#") or stripped.startswith("```"):
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
top_match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$", stripped)
|
|
271
|
+
if top_match:
|
|
272
|
+
key = top_match.group(1)
|
|
273
|
+
value = top_match.group(2).strip()
|
|
274
|
+
if key in top_level_keys:
|
|
275
|
+
started = True
|
|
276
|
+
section = key if key == "quality_gates" else None
|
|
277
|
+
list_key = key if key == "optional_capabilities" and not value else None
|
|
278
|
+
if key == "quality_gates":
|
|
279
|
+
contract.setdefault("quality_gates", {})
|
|
280
|
+
elif key == "optional_capabilities":
|
|
281
|
+
if value:
|
|
282
|
+
contract[key] = _parse_inline_list(value)
|
|
283
|
+
else:
|
|
284
|
+
contract[key] = []
|
|
285
|
+
else:
|
|
286
|
+
contract[key] = value
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
if not started:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
nested_match = re.match(r"^\s+([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$", line)
|
|
293
|
+
if nested_match and section == "quality_gates":
|
|
294
|
+
key = nested_match.group(1)
|
|
295
|
+
value = nested_match.group(2).strip()
|
|
296
|
+
gates = contract.setdefault("quality_gates", {})
|
|
297
|
+
if value:
|
|
298
|
+
gates[key] = _parse_inline_list(value)
|
|
299
|
+
list_key = None
|
|
300
|
+
else:
|
|
301
|
+
gates[key] = []
|
|
302
|
+
list_key = f"quality_gates.{key}"
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
item_match = re.match(r"^\s*-\s+(.+)$", line)
|
|
306
|
+
if item_match:
|
|
307
|
+
item = item_match.group(1).strip()
|
|
308
|
+
if list_key == "optional_capabilities":
|
|
309
|
+
contract.setdefault("optional_capabilities", []).append(item)
|
|
310
|
+
elif list_key and list_key.startswith("quality_gates."):
|
|
311
|
+
gate_key = list_key.split(".", 1)[1]
|
|
312
|
+
gates = contract.setdefault("quality_gates", {})
|
|
313
|
+
gates.setdefault(gate_key, []).append(item)
|
|
314
|
+
|
|
315
|
+
errors = validate_strategy_contract(contract)
|
|
316
|
+
return contract, errors
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def validate_strategy_contract(contract: dict) -> list[str]:
|
|
320
|
+
"""Validate the parsed Development Strategy Contract."""
|
|
321
|
+
errors: list[str] = []
|
|
322
|
+
required = [
|
|
323
|
+
"execution_mode",
|
|
324
|
+
"isolation",
|
|
325
|
+
"verification_profile",
|
|
326
|
+
"retrieval_profile",
|
|
327
|
+
"optional_capabilities",
|
|
328
|
+
"quality_gates",
|
|
329
|
+
]
|
|
330
|
+
for key in required:
|
|
331
|
+
if key not in contract:
|
|
332
|
+
errors.append(f"Development Strategy Contract missing {key}")
|
|
333
|
+
|
|
334
|
+
execution_mode = contract.get("execution_mode")
|
|
335
|
+
if execution_mode and execution_mode not in ALLOWED_EXECUTION_MODES:
|
|
336
|
+
errors.append(f"invalid execution_mode: {execution_mode}")
|
|
337
|
+
|
|
338
|
+
isolation = contract.get("isolation")
|
|
339
|
+
if isolation and isolation not in ALLOWED_ISOLATION:
|
|
340
|
+
errors.append(f"invalid isolation: {isolation}")
|
|
341
|
+
|
|
342
|
+
verification_profile = contract.get("verification_profile")
|
|
343
|
+
if verification_profile and verification_profile not in ALLOWED_VERIFICATION_PROFILES:
|
|
344
|
+
errors.append(f"invalid verification_profile: {verification_profile}")
|
|
345
|
+
|
|
346
|
+
retrieval_profile = contract.get("retrieval_profile")
|
|
347
|
+
if retrieval_profile and retrieval_profile not in ALLOWED_RETRIEVAL_PROFILES:
|
|
348
|
+
errors.append(f"invalid retrieval_profile: {retrieval_profile}")
|
|
349
|
+
|
|
350
|
+
optional_capabilities = contract.get("optional_capabilities")
|
|
351
|
+
if "optional_capabilities" in contract and not isinstance(optional_capabilities, list):
|
|
352
|
+
errors.append("optional_capabilities must be a list")
|
|
353
|
+
|
|
354
|
+
gates = contract.get("quality_gates")
|
|
355
|
+
if isinstance(gates, dict):
|
|
356
|
+
errors.extend(_validate_quality_gates(gates, verification_profile))
|
|
357
|
+
elif "quality_gates" in contract:
|
|
358
|
+
errors.append("quality_gates must be a mapping")
|
|
359
|
+
|
|
360
|
+
return errors
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def enabled_gates_for_contract(contract: dict) -> list[str]:
|
|
364
|
+
"""Return enabled non-baseline gates from a parsed contract."""
|
|
365
|
+
gates = contract.get("quality_gates")
|
|
366
|
+
if not isinstance(gates, dict):
|
|
367
|
+
return []
|
|
368
|
+
|
|
369
|
+
mode = gates.get("mode", "profile")
|
|
370
|
+
if mode == "explicit":
|
|
371
|
+
enabled = gates.get("enabled", [])
|
|
372
|
+
return [g for g in enabled if isinstance(g, str)]
|
|
373
|
+
|
|
374
|
+
profile = gates.get("profile") or contract.get("verification_profile") or "standard"
|
|
375
|
+
return list(PROFILE_DEFAULT_GATES.get(profile, PROFILE_DEFAULT_GATES["standard"]))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def required_gates_for_transition(transition: str, contract: dict) -> list[str]:
|
|
379
|
+
"""Return non-baseline gates required before a transition."""
|
|
380
|
+
enabled = set(enabled_gates_for_contract(contract))
|
|
381
|
+
|
|
382
|
+
if transition == "start-execution":
|
|
383
|
+
required = ["requirements-review"]
|
|
384
|
+
if "architecture-review" in enabled:
|
|
385
|
+
required.append("architecture-review")
|
|
386
|
+
return required
|
|
387
|
+
|
|
388
|
+
if transition in ("full-task-complete", "child-review"):
|
|
389
|
+
required = ["code-review"]
|
|
390
|
+
if "architecture-review" in enabled:
|
|
391
|
+
required.append("architecture-review")
|
|
392
|
+
if "architecture-deep-review" in enabled:
|
|
393
|
+
required.append("architecture-deep-review")
|
|
394
|
+
return required
|
|
395
|
+
|
|
396
|
+
if transition == "parent-integrated":
|
|
397
|
+
return ["integration-review"]
|
|
398
|
+
|
|
399
|
+
return []
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def compute_contract_fingerprint(
|
|
403
|
+
task_dir: Path,
|
|
404
|
+
task_data: dict,
|
|
405
|
+
contract: dict | None = None,
|
|
406
|
+
) -> str:
|
|
407
|
+
"""Compute a task-level fingerprint excluding generated result fields."""
|
|
408
|
+
payload = {
|
|
409
|
+
"schema_version": SCHEMA_VERSION,
|
|
410
|
+
"task_dir": task_dir.name,
|
|
411
|
+
"stable_task": _stable_task_data(task_data),
|
|
412
|
+
"strategy_contract": contract or {},
|
|
413
|
+
}
|
|
414
|
+
return _hash_payload(payload)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def compute_artifact_fingerprint(
|
|
418
|
+
task_dir: Path,
|
|
419
|
+
task_data: dict,
|
|
420
|
+
transition: str,
|
|
421
|
+
gate: str,
|
|
422
|
+
) -> str:
|
|
423
|
+
"""Compute a transition/gate-scoped artifact fingerprint."""
|
|
424
|
+
files = _artifact_files_for(transition, gate)
|
|
425
|
+
payload = {
|
|
426
|
+
"schema_version": SCHEMA_VERSION,
|
|
427
|
+
"transition": transition,
|
|
428
|
+
"gate": gate,
|
|
429
|
+
"task_dir": task_dir.name,
|
|
430
|
+
"stable_task": _stable_task_data(task_data),
|
|
431
|
+
"parent_contract": _parent_contract_metadata(
|
|
432
|
+
task_dir,
|
|
433
|
+
task_data,
|
|
434
|
+
transition,
|
|
435
|
+
),
|
|
436
|
+
"reviewed_change_set": _reviewed_change_set_metadata(
|
|
437
|
+
task_dir,
|
|
438
|
+
transition,
|
|
439
|
+
),
|
|
440
|
+
"files": _read_artifact_files(task_dir, files),
|
|
441
|
+
}
|
|
442
|
+
return _hash_payload(payload)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def make_baseline_record(
|
|
446
|
+
task_dir: Path,
|
|
447
|
+
task_data: dict,
|
|
448
|
+
transition: str,
|
|
449
|
+
contract: dict | None = None,
|
|
450
|
+
evidence: str = "task.json",
|
|
451
|
+
) -> dict:
|
|
452
|
+
"""Build a CLI-owned baseline-check PASS record."""
|
|
453
|
+
contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
|
|
454
|
+
artifact_fingerprint = compute_artifact_fingerprint(
|
|
455
|
+
task_dir, task_data, transition, BASELINE_GATE
|
|
456
|
+
)
|
|
457
|
+
return {
|
|
458
|
+
"schema_version": SCHEMA_VERSION,
|
|
459
|
+
"transition": transition,
|
|
460
|
+
"gate": BASELINE_GATE,
|
|
461
|
+
"result": "PASS",
|
|
462
|
+
"reviewer": "cstl-cli",
|
|
463
|
+
"evidence": evidence,
|
|
464
|
+
"checked_at": utc_now(),
|
|
465
|
+
"contract_fingerprint": contract_fingerprint,
|
|
466
|
+
"artifact_fingerprint": artifact_fingerprint,
|
|
467
|
+
"issue_fingerprint": None,
|
|
468
|
+
"consecutive_failures": 0,
|
|
469
|
+
"approved_skip": None,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def write_gate_record(task_data: dict, transition: str, gate: str, record: dict) -> None:
|
|
474
|
+
"""Write a gate record into task_data in-place."""
|
|
475
|
+
qgr = task_data.get("quality_gate_results")
|
|
476
|
+
if not isinstance(qgr, dict):
|
|
477
|
+
qgr = {}
|
|
478
|
+
task_data["quality_gate_results"] = qgr
|
|
479
|
+
|
|
480
|
+
qgr["schema_version"] = SCHEMA_VERSION
|
|
481
|
+
qgr["contract_fingerprint"] = record.get("contract_fingerprint")
|
|
482
|
+
qgr["artifact_fingerprint"] = record.get("artifact_fingerprint")
|
|
483
|
+
transitions = qgr.get("transitions")
|
|
484
|
+
if not isinstance(transitions, dict):
|
|
485
|
+
transitions = {}
|
|
486
|
+
qgr["transitions"] = transitions
|
|
487
|
+
|
|
488
|
+
transition_records = transitions.get(transition)
|
|
489
|
+
if not isinstance(transition_records, dict):
|
|
490
|
+
transition_records = {}
|
|
491
|
+
transitions[transition] = transition_records
|
|
492
|
+
transition_records[gate] = record
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def build_reviewer_gate_record(
|
|
496
|
+
task_dir: Path,
|
|
497
|
+
task_data: dict,
|
|
498
|
+
transition: str,
|
|
499
|
+
gate: str,
|
|
500
|
+
result: str,
|
|
501
|
+
reviewer: str,
|
|
502
|
+
evidence: str,
|
|
503
|
+
issue_fingerprint: str | None = None,
|
|
504
|
+
issue_summary: str | None = None,
|
|
505
|
+
root_cause: str | None = None,
|
|
506
|
+
skip_approved_by: str | None = None,
|
|
507
|
+
skip_reason: str | None = None,
|
|
508
|
+
contract_fingerprint: str | None = None,
|
|
509
|
+
artifact_fingerprint: str | None = None,
|
|
510
|
+
) -> tuple[dict | None, list[str], list[str]]:
|
|
511
|
+
"""Validate and build a non-baseline reviewer gate record."""
|
|
512
|
+
normalized_result = normalize_result(result)
|
|
513
|
+
errors = validate_reviewer_gate_input(
|
|
514
|
+
transition=transition,
|
|
515
|
+
gate=gate,
|
|
516
|
+
result=normalized_result,
|
|
517
|
+
reviewer=reviewer,
|
|
518
|
+
evidence=evidence,
|
|
519
|
+
issue_fingerprint=issue_fingerprint,
|
|
520
|
+
issue_summary=issue_summary,
|
|
521
|
+
root_cause=root_cause,
|
|
522
|
+
skip_approved_by=skip_approved_by,
|
|
523
|
+
skip_reason=skip_reason,
|
|
524
|
+
)
|
|
525
|
+
warnings: list[str] = []
|
|
526
|
+
if errors:
|
|
527
|
+
return None, errors, warnings
|
|
528
|
+
|
|
529
|
+
if normalized_result in ("PASS", "SKIPPED"):
|
|
530
|
+
errors.extend(
|
|
531
|
+
validate_transition_readiness(
|
|
532
|
+
task_dir,
|
|
533
|
+
task_data,
|
|
534
|
+
transition,
|
|
535
|
+
gate=gate,
|
|
536
|
+
mode="record",
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
if errors:
|
|
540
|
+
return None, errors, warnings
|
|
541
|
+
|
|
542
|
+
contract, contract_errors = resolve_strategy_contract_for_gates(task_dir, task_data)
|
|
543
|
+
if contract_errors:
|
|
544
|
+
errors.extend(contract_errors)
|
|
545
|
+
return None, errors, warnings
|
|
546
|
+
|
|
547
|
+
current_contract_fingerprint = compute_contract_fingerprint(
|
|
548
|
+
task_dir, task_data, contract
|
|
549
|
+
)
|
|
550
|
+
current_artifact_fingerprint = compute_artifact_fingerprint(
|
|
551
|
+
task_dir, task_data, transition, gate
|
|
552
|
+
)
|
|
553
|
+
if contract_fingerprint and contract_fingerprint != current_contract_fingerprint:
|
|
554
|
+
errors.append("provided contract fingerprint does not match current task artifacts")
|
|
555
|
+
if artifact_fingerprint and artifact_fingerprint != current_artifact_fingerprint:
|
|
556
|
+
errors.append("provided artifact fingerprint does not match current task artifacts")
|
|
557
|
+
if errors:
|
|
558
|
+
return None, errors, warnings
|
|
559
|
+
|
|
560
|
+
previous = _get_gate_record(task_data, transition, gate)
|
|
561
|
+
consecutive_failures = 0
|
|
562
|
+
required_user_choice = None
|
|
563
|
+
if normalized_result == "FAIL":
|
|
564
|
+
consecutive_failures = _next_consecutive_failures(previous, issue_fingerprint)
|
|
565
|
+
if consecutive_failures > 3:
|
|
566
|
+
required_user_choice = {
|
|
567
|
+
"required": True,
|
|
568
|
+
"reason": "same gate and issue fingerprint failed more than three times",
|
|
569
|
+
"options": [
|
|
570
|
+
"re-plan",
|
|
571
|
+
"continue-fixing",
|
|
572
|
+
"user-approved-skip-if-allowed",
|
|
573
|
+
],
|
|
574
|
+
}
|
|
575
|
+
warnings.append(
|
|
576
|
+
"same gate and issue fingerprint has failed more than three times; ask the user to choose re-plan, continue fixing, or user-approved skip if allowed"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
approved_skip = None
|
|
580
|
+
if normalized_result == "SKIPPED":
|
|
581
|
+
approved_skip = {
|
|
582
|
+
"approved_by": skip_approved_by,
|
|
583
|
+
"reason": skip_reason,
|
|
584
|
+
"approved_at": utc_now(),
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
record = {
|
|
588
|
+
"schema_version": SCHEMA_VERSION,
|
|
589
|
+
"transition": transition,
|
|
590
|
+
"gate": gate,
|
|
591
|
+
"result": normalized_result,
|
|
592
|
+
"reviewer": reviewer,
|
|
593
|
+
"evidence": evidence,
|
|
594
|
+
"checked_at": utc_now(),
|
|
595
|
+
"contract_fingerprint": current_contract_fingerprint,
|
|
596
|
+
"artifact_fingerprint": current_artifact_fingerprint,
|
|
597
|
+
"issue_fingerprint": issue_fingerprint if normalized_result == "FAIL" else None,
|
|
598
|
+
"root_cause": root_cause if normalized_result == "FAIL" else None,
|
|
599
|
+
"route": _route_for_fail_root_cause(root_cause) if normalized_result == "FAIL" else None,
|
|
600
|
+
"consecutive_failures": consecutive_failures,
|
|
601
|
+
"required_user_choice": required_user_choice,
|
|
602
|
+
"approved_skip": approved_skip,
|
|
603
|
+
}
|
|
604
|
+
if issue_summary and normalized_result == "FAIL":
|
|
605
|
+
record["issue_summary"] = issue_summary
|
|
606
|
+
return record, [], warnings
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def validate_start_execution(
|
|
610
|
+
task_dir: Path,
|
|
611
|
+
task_data: dict | None,
|
|
612
|
+
approved: bool,
|
|
613
|
+
) -> GateGuardResult:
|
|
614
|
+
"""Validate start-execution readiness."""
|
|
615
|
+
errors: list[str] = []
|
|
616
|
+
if task_data is None:
|
|
617
|
+
return GateGuardResult(ok=False, errors=["task.json"])
|
|
618
|
+
|
|
619
|
+
status = task_data.get("status")
|
|
620
|
+
if status not in ("planning", "in_progress"):
|
|
621
|
+
errors.append(f"task status must be planning or in_progress, got {status!r}")
|
|
622
|
+
|
|
623
|
+
errors.extend(_required_file_errors(task_dir, ["prd.md"]))
|
|
624
|
+
|
|
625
|
+
has_design = (task_dir / "design.md").is_file()
|
|
626
|
+
has_implement = (task_dir / "implement.md").is_file()
|
|
627
|
+
if has_design != has_implement:
|
|
628
|
+
errors.append("design.md and implement.md must be present together for Full Tasks")
|
|
629
|
+
|
|
630
|
+
full_task = is_full_task(task_dir, task_data)
|
|
631
|
+
contract: dict = {}
|
|
632
|
+
required_gates: list[str] = []
|
|
633
|
+
if full_task:
|
|
634
|
+
contract, contract_errors = read_strategy_contract(task_dir)
|
|
635
|
+
errors.extend(contract_errors)
|
|
636
|
+
if not contract_errors:
|
|
637
|
+
required_gates = required_gates_for_transition("start-execution", contract)
|
|
638
|
+
|
|
639
|
+
contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
|
|
640
|
+
artifact_fingerprints = {
|
|
641
|
+
BASELINE_GATE: compute_artifact_fingerprint(
|
|
642
|
+
task_dir, task_data, "start-execution", BASELINE_GATE
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
auto_gate_records: dict[str, dict] = {}
|
|
647
|
+
for gate in required_gates:
|
|
648
|
+
artifact_fingerprints[gate] = compute_artifact_fingerprint(
|
|
649
|
+
task_dir, task_data, "start-execution", gate
|
|
650
|
+
)
|
|
651
|
+
gate_errors = _validate_gate_record_for_transition(
|
|
652
|
+
task_data=task_data,
|
|
653
|
+
transition="start-execution",
|
|
654
|
+
gate=gate,
|
|
655
|
+
contract_fingerprint=contract_fingerprint,
|
|
656
|
+
artifact_fingerprint=artifact_fingerprints[gate],
|
|
657
|
+
)
|
|
658
|
+
if gate_errors and gate in START_EXECUTION_AUTO_GATES and _only_missing_gate_record(
|
|
659
|
+
gate_errors
|
|
660
|
+
):
|
|
661
|
+
readiness_errors = _start_execution_planning_gate_readiness_errors(
|
|
662
|
+
task_dir,
|
|
663
|
+
gate,
|
|
664
|
+
full_task=full_task,
|
|
665
|
+
contract=contract,
|
|
666
|
+
)
|
|
667
|
+
if readiness_errors:
|
|
668
|
+
errors.extend(readiness_errors)
|
|
669
|
+
else:
|
|
670
|
+
auto_gate_records[gate] = make_planning_review_gate_record(
|
|
671
|
+
task_dir=task_dir,
|
|
672
|
+
task_data=task_data,
|
|
673
|
+
transition="start-execution",
|
|
674
|
+
gate=gate,
|
|
675
|
+
contract=contract,
|
|
676
|
+
contract_fingerprint=contract_fingerprint,
|
|
677
|
+
artifact_fingerprint=artifact_fingerprints[gate],
|
|
678
|
+
)
|
|
679
|
+
else:
|
|
680
|
+
errors.extend(gate_errors)
|
|
681
|
+
|
|
682
|
+
if approved:
|
|
683
|
+
errors.extend(
|
|
684
|
+
_validate_existing_execution_approval(
|
|
685
|
+
task_data=task_data,
|
|
686
|
+
contract_fingerprint=contract_fingerprint,
|
|
687
|
+
artifact_fingerprint=artifact_fingerprints[BASELINE_GATE],
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
if not approved:
|
|
692
|
+
errors.append("--approved is required for mutation")
|
|
693
|
+
|
|
694
|
+
baseline_record = None
|
|
695
|
+
if not errors:
|
|
696
|
+
baseline_record = make_baseline_record(
|
|
697
|
+
task_dir, task_data, "start-execution", contract
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
return GateGuardResult(
|
|
701
|
+
ok=not errors,
|
|
702
|
+
errors=errors,
|
|
703
|
+
contract_fingerprint=contract_fingerprint,
|
|
704
|
+
artifact_fingerprints=artifact_fingerprints,
|
|
705
|
+
required_gates=required_gates,
|
|
706
|
+
baseline_record=baseline_record,
|
|
707
|
+
is_full_task=full_task,
|
|
708
|
+
auto_gate_records=auto_gate_records,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _validate_existing_execution_approval(
|
|
713
|
+
task_data: dict,
|
|
714
|
+
contract_fingerprint: str,
|
|
715
|
+
artifact_fingerprint: str,
|
|
716
|
+
) -> list[str]:
|
|
717
|
+
"""Reject stale approval state before re-approving an in-progress task."""
|
|
718
|
+
approval = task_data.get("execution_approval")
|
|
719
|
+
if not isinstance(approval, dict):
|
|
720
|
+
return []
|
|
721
|
+
|
|
722
|
+
errors: list[str] = []
|
|
723
|
+
if approval.get("transition") != "start-execution":
|
|
724
|
+
errors.append("stale execution approval: transition mismatch")
|
|
725
|
+
if approval.get("contract_fingerprint") != contract_fingerprint:
|
|
726
|
+
errors.append("stale execution approval: contract fingerprint changed")
|
|
727
|
+
if approval.get("artifact_fingerprint") != artifact_fingerprint:
|
|
728
|
+
errors.append("stale execution approval: artifact fingerprint changed")
|
|
729
|
+
if errors:
|
|
730
|
+
errors.append(
|
|
731
|
+
"return to Planning, refresh gates, run start-execution --check, and ask for explicit approval again"
|
|
732
|
+
)
|
|
733
|
+
return errors
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def validate_start_execution_check(
|
|
737
|
+
task_dir: Path,
|
|
738
|
+
task_data: dict | None,
|
|
739
|
+
) -> GateGuardResult:
|
|
740
|
+
"""Validate start-execution preflight without requiring approval."""
|
|
741
|
+
result = validate_start_execution(task_dir, task_data, approved=True)
|
|
742
|
+
return result
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def validate_transition_readiness(
|
|
746
|
+
task_dir: Path,
|
|
747
|
+
task_data: dict | None,
|
|
748
|
+
transition: str,
|
|
749
|
+
*,
|
|
750
|
+
gate: str | None = None,
|
|
751
|
+
mode: str = "record",
|
|
752
|
+
) -> list[str]:
|
|
753
|
+
"""Validate transition evidence and, in complete mode, required gate records."""
|
|
754
|
+
if task_data is None:
|
|
755
|
+
return ["task.json"]
|
|
756
|
+
|
|
757
|
+
errors: list[str] = []
|
|
758
|
+
if transition not in KNOWN_TRANSITIONS:
|
|
759
|
+
errors.append(f"unknown transition: {transition}")
|
|
760
|
+
return errors
|
|
761
|
+
|
|
762
|
+
profile = task_closeout_profile(task_dir, task_data)
|
|
763
|
+
|
|
764
|
+
if mode == "record":
|
|
765
|
+
if not gate:
|
|
766
|
+
errors.append("gate is required for record readiness checks")
|
|
767
|
+
return errors
|
|
768
|
+
required_signals = _evidence_requirements_for_gate(transition, gate)
|
|
769
|
+
errors.extend(_evidence_errors_for_signals(task_dir, task_data, required_signals))
|
|
770
|
+
return errors
|
|
771
|
+
|
|
772
|
+
if transition == "child-review":
|
|
773
|
+
if profile != "full":
|
|
774
|
+
return errors
|
|
775
|
+
contract, contract_errors = read_strategy_contract(task_dir)
|
|
776
|
+
errors.extend(contract_errors)
|
|
777
|
+
if contract_errors:
|
|
778
|
+
return errors
|
|
779
|
+
return errors + _complete_transition_gate_errors(
|
|
780
|
+
task_dir,
|
|
781
|
+
task_data,
|
|
782
|
+
transition,
|
|
783
|
+
contract,
|
|
784
|
+
required_gates_for_transition(transition, contract),
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
if transition == "full-task-complete":
|
|
788
|
+
if profile != "full":
|
|
789
|
+
return errors
|
|
790
|
+
contract, contract_errors = read_strategy_contract(task_dir)
|
|
791
|
+
errors.extend(contract_errors)
|
|
792
|
+
if contract_errors:
|
|
793
|
+
return errors
|
|
794
|
+
return errors + _complete_transition_gate_errors(
|
|
795
|
+
task_dir,
|
|
796
|
+
task_data,
|
|
797
|
+
transition,
|
|
798
|
+
contract,
|
|
799
|
+
required_gates_for_transition(transition, contract),
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
if transition == "parent-integrated":
|
|
803
|
+
if profile != "parent":
|
|
804
|
+
return errors
|
|
805
|
+
child_names = task_data.get("children")
|
|
806
|
+
if isinstance(child_names, list):
|
|
807
|
+
errors.extend(
|
|
808
|
+
validate_parent_children_complete(
|
|
809
|
+
task_dir,
|
|
810
|
+
[name for name in child_names if isinstance(name, str)],
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
errors.extend(
|
|
814
|
+
_evidence_errors_for_signals(task_dir, task_data, ["integration"])
|
|
815
|
+
)
|
|
816
|
+
contract, contract_errors = resolve_strategy_contract_for_gates(
|
|
817
|
+
task_dir,
|
|
818
|
+
task_data,
|
|
819
|
+
)
|
|
820
|
+
errors.extend(contract_errors)
|
|
821
|
+
if contract_errors:
|
|
822
|
+
return errors
|
|
823
|
+
return errors + _complete_transition_gate_errors(
|
|
824
|
+
task_dir,
|
|
825
|
+
task_data,
|
|
826
|
+
transition,
|
|
827
|
+
contract,
|
|
828
|
+
required_gates_for_transition(transition, contract),
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
return errors
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _complete_transition_gate_errors(
|
|
835
|
+
task_dir: Path,
|
|
836
|
+
task_data: dict,
|
|
837
|
+
transition: str,
|
|
838
|
+
contract: dict,
|
|
839
|
+
required_gates: list[str],
|
|
840
|
+
) -> list[str]:
|
|
841
|
+
errors: list[str] = []
|
|
842
|
+
contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
|
|
843
|
+
for gate_name in required_gates:
|
|
844
|
+
artifact_fingerprint = compute_artifact_fingerprint(
|
|
845
|
+
task_dir, task_data, transition, gate_name
|
|
846
|
+
)
|
|
847
|
+
errors.extend(
|
|
848
|
+
_validate_gate_record_for_transition(
|
|
849
|
+
task_data=task_data,
|
|
850
|
+
transition=transition,
|
|
851
|
+
gate=gate_name,
|
|
852
|
+
contract_fingerprint=contract_fingerprint,
|
|
853
|
+
artifact_fingerprint=artifact_fingerprint,
|
|
854
|
+
)
|
|
855
|
+
)
|
|
856
|
+
errors.extend(
|
|
857
|
+
_evidence_errors_for_signals(
|
|
858
|
+
task_dir,
|
|
859
|
+
task_data,
|
|
860
|
+
_evidence_requirements_for_gate(transition, gate_name),
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
return errors
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _evidence_requirements_for_gate(transition: str, gate: str) -> list[str]:
|
|
867
|
+
if gate == "code-review" and transition in ("full-task-complete", "child-review"):
|
|
868
|
+
return ["validation", "check_evidence", "reviewed_change_set"]
|
|
869
|
+
if gate == "integration-review" and transition == "parent-integrated":
|
|
870
|
+
return ["integration"]
|
|
871
|
+
if gate in ("architecture-review", "architecture-deep-review") and transition in (
|
|
872
|
+
"full-task-complete",
|
|
873
|
+
"child-review",
|
|
874
|
+
):
|
|
875
|
+
return ["validation", "check_evidence"]
|
|
876
|
+
return []
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _evidence_errors_for_signals(
|
|
880
|
+
task_dir: Path,
|
|
881
|
+
task_data: dict,
|
|
882
|
+
required_signals: list[str],
|
|
883
|
+
) -> list[str]:
|
|
884
|
+
status = verify_evidence_status(task_dir, task_data)
|
|
885
|
+
label_map = {
|
|
886
|
+
"validation": "verify.md missing substantive validation evidence",
|
|
887
|
+
"check_evidence": "verify.md missing check evidence",
|
|
888
|
+
"acceptance": "verify.md missing final acceptance evidence",
|
|
889
|
+
"durable_learning": "verify.md missing durable-learning decision evidence",
|
|
890
|
+
"integration": "verify.md missing final integration evidence",
|
|
891
|
+
"reviewed_change_set": (
|
|
892
|
+
"verify.md or handoff.md missing reviewed change-set evidence"
|
|
893
|
+
),
|
|
894
|
+
}
|
|
895
|
+
errors: list[str] = []
|
|
896
|
+
for signal in required_signals:
|
|
897
|
+
if not status.get(signal):
|
|
898
|
+
errors.append(label_map.get(signal, f"missing evidence signal: {signal}"))
|
|
899
|
+
return errors
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def verify_evidence_status(task_dir: Path, task_data: dict) -> dict[str, bool]:
|
|
903
|
+
"""Return substantive evidence signals from verify.md and handoff.md."""
|
|
904
|
+
verify_path = task_dir / "verify.md"
|
|
905
|
+
content = ""
|
|
906
|
+
if verify_path.is_file():
|
|
907
|
+
try:
|
|
908
|
+
content = verify_path.read_text(encoding="utf-8")
|
|
909
|
+
except OSError:
|
|
910
|
+
content = ""
|
|
911
|
+
|
|
912
|
+
handoff_path = task_dir / "handoff.md"
|
|
913
|
+
handoff_content = ""
|
|
914
|
+
if handoff_path.is_file():
|
|
915
|
+
try:
|
|
916
|
+
handoff_content = handoff_path.read_text(encoding="utf-8")
|
|
917
|
+
except OSError:
|
|
918
|
+
handoff_content = ""
|
|
919
|
+
|
|
920
|
+
child_names = task_data.get("children")
|
|
921
|
+
has_children = isinstance(child_names, list) and bool(child_names)
|
|
922
|
+
return {
|
|
923
|
+
"validation": _has_substantive_validation_evidence(content),
|
|
924
|
+
"check_evidence": _has_substantive_check_evidence(content),
|
|
925
|
+
"acceptance": _has_substantive_acceptance_evidence(content),
|
|
926
|
+
"durable_learning": _has_durable_learning_evidence(content),
|
|
927
|
+
"integration": (
|
|
928
|
+
_has_substantive_integration_evidence(content) if has_children else True
|
|
929
|
+
),
|
|
930
|
+
"reviewed_change_set": (
|
|
931
|
+
_has_substantive_reviewed_change_set(content)
|
|
932
|
+
or _has_substantive_reviewed_change_set(handoff_content)
|
|
933
|
+
),
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def validate_archive(
|
|
938
|
+
task_dir: Path,
|
|
939
|
+
task_data: dict | None,
|
|
940
|
+
) -> GateGuardResult:
|
|
941
|
+
"""Validate archive readiness without mutating task files."""
|
|
942
|
+
errors: list[str] = []
|
|
943
|
+
if task_data is None:
|
|
944
|
+
return GateGuardResult(ok=False, errors=["task.json"])
|
|
945
|
+
|
|
946
|
+
errors.extend(_required_file_errors(task_dir, ["verify.md"]))
|
|
947
|
+
errors.extend(_verify_evidence_errors(task_dir, task_data))
|
|
948
|
+
|
|
949
|
+
parent_name = task_data.get("parent")
|
|
950
|
+
if isinstance(parent_name, str) and parent_name:
|
|
951
|
+
parent_dir = task_dir.parent / parent_name
|
|
952
|
+
if not parent_dir.is_dir():
|
|
953
|
+
errors.append(f"parent task not found: {parent_name}")
|
|
954
|
+
else:
|
|
955
|
+
errors.extend(validate_child_archive_state(parent_dir, task_dir.name))
|
|
956
|
+
if get_child_state(parent_dir, task_dir.name) == "integrated":
|
|
957
|
+
errors.extend(_required_file_errors(task_dir, ["handoff.md"]))
|
|
958
|
+
|
|
959
|
+
child_names = task_data.get("children")
|
|
960
|
+
if isinstance(child_names, list) and child_names:
|
|
961
|
+
errors.extend(
|
|
962
|
+
validate_parent_children_complete(
|
|
963
|
+
task_dir,
|
|
964
|
+
[name for name in child_names if isinstance(name, str)],
|
|
965
|
+
)
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
profile = task_closeout_profile(task_dir, task_data)
|
|
969
|
+
contract: dict = {}
|
|
970
|
+
required_gates: list[str] = []
|
|
971
|
+
artifact_fingerprints: dict[str, str] = {}
|
|
972
|
+
baseline_record = None
|
|
973
|
+
|
|
974
|
+
if profile == "full":
|
|
975
|
+
contract, contract_errors = read_strategy_contract(task_dir)
|
|
976
|
+
errors.extend(contract_errors)
|
|
977
|
+
if not contract_errors:
|
|
978
|
+
required_gates = required_gates_for_transition(
|
|
979
|
+
"full-task-complete", contract
|
|
980
|
+
)
|
|
981
|
+
errors.extend(
|
|
982
|
+
validate_transition_readiness(
|
|
983
|
+
task_dir,
|
|
984
|
+
task_data,
|
|
985
|
+
"full-task-complete",
|
|
986
|
+
mode="complete",
|
|
987
|
+
)
|
|
988
|
+
)
|
|
989
|
+
elif profile == "parent":
|
|
990
|
+
contract, contract_errors = resolve_strategy_contract_for_gates(
|
|
991
|
+
task_dir,
|
|
992
|
+
task_data,
|
|
993
|
+
)
|
|
994
|
+
errors.extend(contract_errors)
|
|
995
|
+
if not contract_errors:
|
|
996
|
+
errors.extend(
|
|
997
|
+
validate_transition_readiness(
|
|
998
|
+
task_dir,
|
|
999
|
+
task_data,
|
|
1000
|
+
"parent-integrated",
|
|
1001
|
+
mode="complete",
|
|
1002
|
+
)
|
|
1003
|
+
)
|
|
1004
|
+
required_gates = required_gates_for_transition("parent-integrated", contract)
|
|
1005
|
+
|
|
1006
|
+
contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
|
|
1007
|
+
|
|
1008
|
+
if profile == "full" and not errors:
|
|
1009
|
+
artifact_fingerprints[BASELINE_GATE] = compute_artifact_fingerprint(
|
|
1010
|
+
task_dir, task_data, "full-task-complete", BASELINE_GATE
|
|
1011
|
+
)
|
|
1012
|
+
for gate in required_gates:
|
|
1013
|
+
artifact_fingerprints[gate] = compute_artifact_fingerprint(
|
|
1014
|
+
task_dir, task_data, "full-task-complete", gate
|
|
1015
|
+
)
|
|
1016
|
+
baseline_record = make_baseline_record(
|
|
1017
|
+
task_dir, task_data, "full-task-complete", contract
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
return GateGuardResult(
|
|
1021
|
+
ok=not errors,
|
|
1022
|
+
errors=errors,
|
|
1023
|
+
contract_fingerprint=contract_fingerprint,
|
|
1024
|
+
artifact_fingerprints=artifact_fingerprints,
|
|
1025
|
+
required_gates=required_gates,
|
|
1026
|
+
baseline_record=baseline_record,
|
|
1027
|
+
is_full_task=profile == "full",
|
|
1028
|
+
closeout_profile=profile,
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _verify_evidence_errors(task_dir: Path, task_data: dict) -> list[str]:
|
|
1033
|
+
"""Validate human-readable archive evidence in verify.md."""
|
|
1034
|
+
verify_path = task_dir / "verify.md"
|
|
1035
|
+
if not verify_path.is_file():
|
|
1036
|
+
return []
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
verify_path.read_text(encoding="utf-8")
|
|
1040
|
+
except OSError:
|
|
1041
|
+
return ["verify.md could not be read for archive evidence"]
|
|
1042
|
+
|
|
1043
|
+
errors: list[str] = []
|
|
1044
|
+
status = verify_evidence_status(task_dir, task_data)
|
|
1045
|
+
if not status["validation"]:
|
|
1046
|
+
errors.append("verify.md missing validation evidence")
|
|
1047
|
+
if not status["acceptance"]:
|
|
1048
|
+
errors.append("verify.md missing final acceptance evidence")
|
|
1049
|
+
if not status["durable_learning"]:
|
|
1050
|
+
errors.append("verify.md missing durable-learning decision evidence")
|
|
1051
|
+
if not status["integration"]:
|
|
1052
|
+
errors.append("verify.md missing final integration evidence")
|
|
1053
|
+
|
|
1054
|
+
return errors
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _is_substantive_evidence(value: str | None) -> bool:
|
|
1058
|
+
if not value or not isinstance(value, str):
|
|
1059
|
+
return False
|
|
1060
|
+
stripped = value.strip()
|
|
1061
|
+
if len(stripped) < 3:
|
|
1062
|
+
return False
|
|
1063
|
+
return not PLACEHOLDER_VALUES_RE.match(stripped)
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _has_substantive_line_match(content: str, pattern: re.Pattern[str]) -> bool:
|
|
1067
|
+
if not content:
|
|
1068
|
+
return False
|
|
1069
|
+
for match in pattern.finditer(content):
|
|
1070
|
+
if match.lastindex and match.lastindex >= 1:
|
|
1071
|
+
if _is_substantive_evidence(match.group(1)):
|
|
1072
|
+
return True
|
|
1073
|
+
continue
|
|
1074
|
+
line = match.group(0)
|
|
1075
|
+
if ":" in line:
|
|
1076
|
+
value = line.split(":", 1)[1].strip()
|
|
1077
|
+
if _is_substantive_evidence(value):
|
|
1078
|
+
return True
|
|
1079
|
+
return False
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _has_substantive_validation_evidence(content: str) -> bool:
|
|
1083
|
+
return _has_substantive_line_match(content, VALIDATION_EVIDENCE_RE)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _has_substantive_check_evidence(content: str) -> bool:
|
|
1087
|
+
return _has_substantive_line_match(content, CHECK_EVIDENCE_RE)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _has_substantive_acceptance_evidence(content: str) -> bool:
|
|
1091
|
+
return (
|
|
1092
|
+
_has_substantive_line_match(content, ACCEPTANCE_EVIDENCE_RE)
|
|
1093
|
+
or _has_substantive_line_match(content, ACCEPTED_BY_USER_RE)
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _has_substantive_integration_evidence(content: str) -> bool:
|
|
1098
|
+
return _has_substantive_line_match(content, INTEGRATION_EVIDENCE_RE)
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _has_substantive_reviewed_change_set(content: str) -> bool:
|
|
1102
|
+
if not content:
|
|
1103
|
+
return False
|
|
1104
|
+
for match in REVIEWED_CHANGE_SET_RE.finditer(content):
|
|
1105
|
+
if _is_substantive_evidence(match.group(1)):
|
|
1106
|
+
return True
|
|
1107
|
+
return False
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _has_validation_evidence(content: str) -> bool:
|
|
1111
|
+
return _has_substantive_validation_evidence(content)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def _has_final_acceptance_evidence(content: str) -> bool:
|
|
1115
|
+
return _has_substantive_acceptance_evidence(content)
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _has_durable_learning_evidence(content: str) -> bool:
|
|
1119
|
+
return bool(
|
|
1120
|
+
NO_DURABLE_LEARNING_RE.search(content)
|
|
1121
|
+
or _has_substantive_line_match(content, DURABLE_LEARNING_EVIDENCE_RE)
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def durable_learning_decision_status(content: str) -> dict[str, bool]:
|
|
1126
|
+
"""Return which durable-learning outcomes are signaled in verify.md text."""
|
|
1127
|
+
return {
|
|
1128
|
+
"no_durable_learning": bool(NO_DURABLE_LEARNING_RE.search(content)),
|
|
1129
|
+
"spec_update": bool(
|
|
1130
|
+
re.search(
|
|
1131
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:spec\s+updates?|spec\s+update\s+"
|
|
1132
|
+
r"(?:needed|evidence)|updated\s+spec)\s*:\s*\S",
|
|
1133
|
+
content,
|
|
1134
|
+
)
|
|
1135
|
+
),
|
|
1136
|
+
"learning_artifact": bool(
|
|
1137
|
+
re.search(
|
|
1138
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:learning\s+artifact|retrospective(?:\.md)?)\s*:\s*\S",
|
|
1139
|
+
content,
|
|
1140
|
+
)
|
|
1141
|
+
or re.search(
|
|
1142
|
+
r"(?im)^\s*(?:[-*]\s*)?(?:durable\s+learning|learning\s+decision)\s*:\s*\S",
|
|
1143
|
+
content,
|
|
1144
|
+
)
|
|
1145
|
+
),
|
|
1146
|
+
"any": _has_durable_learning_evidence(content),
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def suggest_spec_targets(repo_root: Path, task_dir: Path, task_data: dict) -> list[str]:
|
|
1151
|
+
"""Suggest existing spec paths from task scope; does not invent new rules."""
|
|
1152
|
+
suggestions: list[str] = []
|
|
1153
|
+
spec_root = repo_root / ".cstl" / "spec"
|
|
1154
|
+
if not spec_root.is_dir():
|
|
1155
|
+
return suggestions
|
|
1156
|
+
|
|
1157
|
+
package = task_data.get("package")
|
|
1158
|
+
if isinstance(package, str) and package.strip():
|
|
1159
|
+
pkg_index = spec_root / package.strip() / "index.md"
|
|
1160
|
+
if pkg_index.is_file():
|
|
1161
|
+
suggestions.append(f".cstl/spec/{package.strip()}/index.md")
|
|
1162
|
+
|
|
1163
|
+
scope = task_data.get("scope")
|
|
1164
|
+
if isinstance(scope, str) and scope.strip():
|
|
1165
|
+
scope_parts = [p for p in scope.strip().replace("\\", "/").split("/") if p]
|
|
1166
|
+
scope_path = spec_root.joinpath(*scope_parts) if scope_parts else spec_root
|
|
1167
|
+
if scope_path.is_dir():
|
|
1168
|
+
index = scope_path / "index.md"
|
|
1169
|
+
if index.is_file():
|
|
1170
|
+
suggestions.append(
|
|
1171
|
+
f".cstl/spec/{scope_path.relative_to(spec_root).as_posix()}/index.md"
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
guide_paths = (
|
|
1175
|
+
"guides/durable-learning-decision-guide.md",
|
|
1176
|
+
"guides/index.md",
|
|
1177
|
+
)
|
|
1178
|
+
for rel in guide_paths:
|
|
1179
|
+
path = spec_root / rel
|
|
1180
|
+
if path.is_file():
|
|
1181
|
+
suggestions.append(f".cstl/spec/{rel}")
|
|
1182
|
+
|
|
1183
|
+
return _dedupe_preserve_order(suggestions)[:6]
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def build_spec_update_scaffold(
|
|
1187
|
+
repo_root: Path,
|
|
1188
|
+
task_dir: Path,
|
|
1189
|
+
task_data: dict,
|
|
1190
|
+
*,
|
|
1191
|
+
trigger: str | None = None,
|
|
1192
|
+
) -> str:
|
|
1193
|
+
"""Markdown checklist for spec capture; user/reviewer must confirm before editing specs."""
|
|
1194
|
+
targets = suggest_spec_targets(repo_root, task_dir, task_data)
|
|
1195
|
+
rel_task = f".cstl/tasks/{task_dir.name}"
|
|
1196
|
+
lines = [
|
|
1197
|
+
"## Spec update scaffold (reviewer-confirmed)",
|
|
1198
|
+
"",
|
|
1199
|
+
"_Suggestions only — do not treat this block as project policy until a human confirms._",
|
|
1200
|
+
"",
|
|
1201
|
+
]
|
|
1202
|
+
if trigger:
|
|
1203
|
+
lines.extend([f"Trigger: {trigger.strip()}", ""])
|
|
1204
|
+
lines.extend(
|
|
1205
|
+
[
|
|
1206
|
+
"1. Decide outcome in verify.md:",
|
|
1207
|
+
" - Routine: `Durable learning decision: no durable learning`",
|
|
1208
|
+
" - Reusable insight: `Spec update evidence: .cstl/spec/<path>` after edits",
|
|
1209
|
+
f" - Already documented: `Learning artifact: {rel_task}/handoff.md`",
|
|
1210
|
+
"",
|
|
1211
|
+
"2. Use `/cstl:update-spec` or `/cstl:break-loop` for depth; never auto-write specs.",
|
|
1212
|
+
"",
|
|
1213
|
+
]
|
|
1214
|
+
)
|
|
1215
|
+
if targets:
|
|
1216
|
+
lines.append("3. Existing spec indexes to consider (from task scope):")
|
|
1217
|
+
for target in targets:
|
|
1218
|
+
lines.append(f" - `{target}`")
|
|
1219
|
+
lines.append("")
|
|
1220
|
+
else:
|
|
1221
|
+
lines.append(
|
|
1222
|
+
"3. Browse `.cstl/spec/<package-or-layer>/index.md` for the right code-spec file."
|
|
1223
|
+
)
|
|
1224
|
+
lines.append("")
|
|
1225
|
+
lines.append(
|
|
1226
|
+
"4. Re-run `python ./.cstl/scripts/task.py archive <task> --check` after verify.md is final."
|
|
1227
|
+
)
|
|
1228
|
+
lines.append("")
|
|
1229
|
+
return "\n".join(lines)
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def _learning_decision_draft_lines(task_dir: Path, task_data: dict) -> list[str]:
|
|
1233
|
+
"""Default durable-learning block for prepare-archive-evidence."""
|
|
1234
|
+
repo_root = task_dir.parent.parent.parent
|
|
1235
|
+
targets = suggest_spec_targets(repo_root, task_dir, task_data)
|
|
1236
|
+
target_hint = targets[0] if targets else ".cstl/spec/<layer>/index.md"
|
|
1237
|
+
return [
|
|
1238
|
+
"Durable learning decision: no durable learning for this task scope.",
|
|
1239
|
+
"",
|
|
1240
|
+
"# Replace the line above with ONE of these before archive:",
|
|
1241
|
+
f"# Spec update evidence: {target_hint}",
|
|
1242
|
+
f"# Learning artifact: .cstl/tasks/{task_dir.name}/handoff.md",
|
|
1243
|
+
"# Spec update needed: (brief reason) — then run /cstl:update-spec and point Spec update evidence at the edited file",
|
|
1244
|
+
"",
|
|
1245
|
+
]
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
ARCHIVE_EVIDENCE_DRAFT_MARKER = "<!-- trellis:archive-evidence-draft -->"
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def _verify_evidence_status(task_dir: Path, task_data: dict) -> dict[str, bool]:
|
|
1252
|
+
"""Return which archive evidence sections are present in verify.md."""
|
|
1253
|
+
status = verify_evidence_status(task_dir, task_data)
|
|
1254
|
+
return {
|
|
1255
|
+
"validation": status["validation"],
|
|
1256
|
+
"acceptance": status["acceptance"],
|
|
1257
|
+
"durable_learning": status["durable_learning"],
|
|
1258
|
+
"integration": status["integration"],
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def archive_repair_hints(
|
|
1263
|
+
errors: list[str],
|
|
1264
|
+
task_dir: Path,
|
|
1265
|
+
task_data: dict,
|
|
1266
|
+
guard: GateGuardResult,
|
|
1267
|
+
) -> list[str]:
|
|
1268
|
+
"""Map archive validation errors to actionable next-step hints."""
|
|
1269
|
+
hints: list[str] = []
|
|
1270
|
+
task_ref = task_dir.name
|
|
1271
|
+
rel_task = f".cstl/tasks/{task_ref}"
|
|
1272
|
+
|
|
1273
|
+
for error in errors:
|
|
1274
|
+
if error == "verify.md":
|
|
1275
|
+
hints.append(
|
|
1276
|
+
f"Create verify.md, then run: python ./.cstl/scripts/task.py "
|
|
1277
|
+
f"prepare-archive-evidence {rel_task}"
|
|
1278
|
+
)
|
|
1279
|
+
continue
|
|
1280
|
+
if error == "verify.md missing validation evidence":
|
|
1281
|
+
hints.append(
|
|
1282
|
+
"Add a grep-friendly line such as "
|
|
1283
|
+
"'Validation commands: <command> — <outcome>' to verify.md, "
|
|
1284
|
+
f"or run: python ./.cstl/scripts/task.py prepare-archive-evidence {rel_task}"
|
|
1285
|
+
)
|
|
1286
|
+
continue
|
|
1287
|
+
if error == "verify.md missing final acceptance evidence":
|
|
1288
|
+
hints.append(
|
|
1289
|
+
"Add 'Final acceptance evidence: <criteria met>' or "
|
|
1290
|
+
"'Accepted by user: <who/when>' to verify.md, "
|
|
1291
|
+
f"or run prepare-archive-evidence {rel_task}"
|
|
1292
|
+
)
|
|
1293
|
+
continue
|
|
1294
|
+
if error == "verify.md missing durable-learning decision evidence":
|
|
1295
|
+
hints.append(
|
|
1296
|
+
"Durable learning decision (pick one grep-friendly line in verify.md): "
|
|
1297
|
+
"'Durable learning decision: no durable learning' for routine work; "
|
|
1298
|
+
"'Spec update evidence: .cstl/spec/<path>' after /cstl:update-spec; "
|
|
1299
|
+
"'Learning artifact: <path>' when handoff/retrospective already captures the insight. "
|
|
1300
|
+
f"Or run: python ./.cstl/scripts/task.py prepare-archive-evidence {rel_task}"
|
|
1301
|
+
)
|
|
1302
|
+
continue
|
|
1303
|
+
if error == "verify.md missing final integration evidence":
|
|
1304
|
+
hints.append(
|
|
1305
|
+
"Parent tasks need 'Final integration evidence: <child handoffs / task-map>'. "
|
|
1306
|
+
f"Run prepare-archive-evidence {rel_task} to draft a section from task-map.md"
|
|
1307
|
+
)
|
|
1308
|
+
continue
|
|
1309
|
+
if error.startswith("missing gate record:"):
|
|
1310
|
+
_rest = error.split(":", 1)[1].strip()
|
|
1311
|
+
_transition, gate = _rest.split("/", 1)
|
|
1312
|
+
transition = _transition.strip()
|
|
1313
|
+
if transition == "parent-integrated":
|
|
1314
|
+
hints.append(
|
|
1315
|
+
f"Parent archive needs record-gate for parent-integrated/{gate}: "
|
|
1316
|
+
f"python ./.cstl/scripts/task.py record-gate {rel_task} "
|
|
1317
|
+
f"--transition parent-integrated --gate {gate} --result PASS "
|
|
1318
|
+
f"--reviewer parent --evidence task-map.md"
|
|
1319
|
+
)
|
|
1320
|
+
elif transition == "child-review":
|
|
1321
|
+
hints.append(
|
|
1322
|
+
f"Full Child acceptance needs record-gate for child-review/{gate}: "
|
|
1323
|
+
f"python ./.cstl/scripts/task.py record-gate {rel_task} "
|
|
1324
|
+
f"--transition child-review --gate {gate} --result PASS "
|
|
1325
|
+
f"--reviewer parent --evidence verify.md"
|
|
1326
|
+
)
|
|
1327
|
+
else:
|
|
1328
|
+
hints.append(
|
|
1329
|
+
f"Record reviewer gate after explicit review (never auto-PASS): "
|
|
1330
|
+
f"python ./.cstl/scripts/task.py record-gate {rel_task} "
|
|
1331
|
+
f"--transition {transition} --gate {gate} --result PASS "
|
|
1332
|
+
f"--reviewer <reviewer-id> --evidence verify.md"
|
|
1333
|
+
)
|
|
1334
|
+
continue
|
|
1335
|
+
if error == "verify.md missing check evidence":
|
|
1336
|
+
hints.append(
|
|
1337
|
+
"Add a grep-friendly line such as "
|
|
1338
|
+
"'Check evidence: <cstl-check summary or manual review notes>' to verify.md"
|
|
1339
|
+
)
|
|
1340
|
+
continue
|
|
1341
|
+
if error == (
|
|
1342
|
+
"verify.md or handoff.md missing reviewed change-set evidence"
|
|
1343
|
+
):
|
|
1344
|
+
hints.append(
|
|
1345
|
+
"Add 'Reviewed change-set: <git ref or diff summary>' to verify.md or handoff.md"
|
|
1346
|
+
)
|
|
1347
|
+
continue
|
|
1348
|
+
if error == "verify.md missing substantive validation evidence":
|
|
1349
|
+
hints.append(
|
|
1350
|
+
"Replace placeholder validation lines with substantive command/outcome text in verify.md"
|
|
1351
|
+
)
|
|
1352
|
+
continue
|
|
1353
|
+
if error.startswith("gate failed:"):
|
|
1354
|
+
hints.append(
|
|
1355
|
+
"Resolve the FAIL gate (fix, re-review, or user-approved SKIPPED) "
|
|
1356
|
+
"before archive."
|
|
1357
|
+
)
|
|
1358
|
+
continue
|
|
1359
|
+
if error.startswith("stale "):
|
|
1360
|
+
hints.append(
|
|
1361
|
+
"Artifacts changed since the gate was recorded. Re-run review and "
|
|
1362
|
+
"record-gate for full-task-complete, then archive --check again."
|
|
1363
|
+
)
|
|
1364
|
+
continue
|
|
1365
|
+
if "child" in error and "integrated or cancelled" in error:
|
|
1366
|
+
hints.append(
|
|
1367
|
+
"Advance each child in parent task-map to integrated or cancelled "
|
|
1368
|
+
"via integrate-child before parent archive."
|
|
1369
|
+
)
|
|
1370
|
+
continue
|
|
1371
|
+
if error == "handoff.md" or error.startswith("handoff.md"):
|
|
1372
|
+
hints.append(
|
|
1373
|
+
"Integrated children require handoff.md on the child task before archive."
|
|
1374
|
+
)
|
|
1375
|
+
continue
|
|
1376
|
+
|
|
1377
|
+
if not hints and not guard.ok:
|
|
1378
|
+
hints.append(
|
|
1379
|
+
f"Run: python ./.cstl/scripts/task.py prepare-archive-evidence {rel_task} "
|
|
1380
|
+
"then archive --check again."
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
if guard.is_full_task and guard.required_gates:
|
|
1384
|
+
missing_reviewer = any(
|
|
1385
|
+
e.startswith("missing gate record: full-task-complete/")
|
|
1386
|
+
for e in errors
|
|
1387
|
+
)
|
|
1388
|
+
if missing_reviewer and not any("record-gate" in h for h in hints):
|
|
1389
|
+
for gate in guard.required_gates:
|
|
1390
|
+
hints.append(
|
|
1391
|
+
f"Full Task archive needs record-gate for full-task-complete/{gate} "
|
|
1392
|
+
"(reviewer action required)."
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
return _dedupe_preserve_order(hints)
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def _dedupe_preserve_order(items: list[str]) -> list[str]:
|
|
1399
|
+
seen: set[str] = set()
|
|
1400
|
+
out: list[str] = []
|
|
1401
|
+
for item in items:
|
|
1402
|
+
if item in seen:
|
|
1403
|
+
continue
|
|
1404
|
+
seen.add(item)
|
|
1405
|
+
out.append(item)
|
|
1406
|
+
return out
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def build_archive_evidence_draft(
|
|
1410
|
+
task_dir: Path,
|
|
1411
|
+
task_data: dict,
|
|
1412
|
+
guard: GateGuardResult | None = None,
|
|
1413
|
+
) -> str:
|
|
1414
|
+
"""Build markdown sections that satisfy archive evidence regex checks."""
|
|
1415
|
+
if guard is None:
|
|
1416
|
+
guard = validate_archive(task_dir, task_data)
|
|
1417
|
+
|
|
1418
|
+
status = _verify_evidence_status(task_dir, task_data)
|
|
1419
|
+
child_names = task_data.get("children")
|
|
1420
|
+
has_children = isinstance(child_names, list) and bool(child_names)
|
|
1421
|
+
lines = [
|
|
1422
|
+
"",
|
|
1423
|
+
"## Archive evidence (draft)",
|
|
1424
|
+
"",
|
|
1425
|
+
ARCHIVE_EVIDENCE_DRAFT_MARKER,
|
|
1426
|
+
"",
|
|
1427
|
+
"_Auto-drafted by prepare-archive-evidence. Edit placeholders before archive._",
|
|
1428
|
+
"",
|
|
1429
|
+
]
|
|
1430
|
+
|
|
1431
|
+
if not status["validation"]:
|
|
1432
|
+
lines.extend(
|
|
1433
|
+
[
|
|
1434
|
+
"Validation commands: (fill in commands and outcomes, e.g. pnpm test — pass)",
|
|
1435
|
+
"",
|
|
1436
|
+
]
|
|
1437
|
+
)
|
|
1438
|
+
if not status["acceptance"]:
|
|
1439
|
+
lines.extend(
|
|
1440
|
+
[
|
|
1441
|
+
"Final acceptance evidence: (describe acceptance criteria met for this task)",
|
|
1442
|
+
"",
|
|
1443
|
+
]
|
|
1444
|
+
)
|
|
1445
|
+
if not status["durable_learning"]:
|
|
1446
|
+
lines.extend(_learning_decision_draft_lines(task_dir, task_data))
|
|
1447
|
+
if has_children and not status["integration"]:
|
|
1448
|
+
summary = _integration_draft_summary(task_dir, child_names)
|
|
1449
|
+
lines.extend(
|
|
1450
|
+
[
|
|
1451
|
+
f"Final integration evidence: {summary}",
|
|
1452
|
+
"",
|
|
1453
|
+
]
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
if guard.is_full_task and guard.required_gates:
|
|
1457
|
+
rel_task = f".cstl/tasks/{task_dir.name}"
|
|
1458
|
+
lines.extend(
|
|
1459
|
+
[
|
|
1460
|
+
"## Completion gate preparation (not recorded)",
|
|
1461
|
+
"",
|
|
1462
|
+
"Reviewer gates are **not** recorded by this helper. After review, run:",
|
|
1463
|
+
"",
|
|
1464
|
+
]
|
|
1465
|
+
)
|
|
1466
|
+
for gate in guard.required_gates:
|
|
1467
|
+
lines.append(
|
|
1468
|
+
f"- `python ./.cstl/scripts/task.py record-gate {rel_task} "
|
|
1469
|
+
f"--transition full-task-complete --gate {gate} --result PASS "
|
|
1470
|
+
f"--reviewer <reviewer-id> --evidence verify.md`"
|
|
1471
|
+
)
|
|
1472
|
+
lines.append("")
|
|
1473
|
+
|
|
1474
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
def _integration_draft_summary(task_dir: Path, child_names: list) -> str:
|
|
1478
|
+
"""Summarize child integration states for a parent integration evidence line."""
|
|
1479
|
+
names = [n for n in child_names if isinstance(n, str)]
|
|
1480
|
+
if not names:
|
|
1481
|
+
return "parent children integrated per task-map.md"
|
|
1482
|
+
|
|
1483
|
+
data, _body = load_task_map(task_dir)
|
|
1484
|
+
states: list[str] = []
|
|
1485
|
+
if isinstance(data, dict):
|
|
1486
|
+
children = data.get("children")
|
|
1487
|
+
if isinstance(children, list):
|
|
1488
|
+
by_id = {
|
|
1489
|
+
entry.get("id"): entry.get("state")
|
|
1490
|
+
for entry in children
|
|
1491
|
+
if isinstance(entry, dict) and entry.get("id")
|
|
1492
|
+
}
|
|
1493
|
+
for name in names:
|
|
1494
|
+
state = by_id.get(name, "unknown")
|
|
1495
|
+
states.append(f"{name}={state}")
|
|
1496
|
+
|
|
1497
|
+
if states:
|
|
1498
|
+
return "children " + ", ".join(states) + " per task-map.md"
|
|
1499
|
+
return "all structural children terminal in task-map.md before parent archive"
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
def prepare_archive_evidence(
|
|
1503
|
+
task_dir: Path,
|
|
1504
|
+
task_data: dict | None,
|
|
1505
|
+
*,
|
|
1506
|
+
dry_run: bool = False,
|
|
1507
|
+
) -> tuple[bool, list[str]]:
|
|
1508
|
+
"""
|
|
1509
|
+
Append missing archive evidence sections to verify.md without rewriting user text.
|
|
1510
|
+
|
|
1511
|
+
Returns (changed, messages).
|
|
1512
|
+
"""
|
|
1513
|
+
messages: list[str] = []
|
|
1514
|
+
if task_data is None:
|
|
1515
|
+
return False, ["task.json missing or invalid"]
|
|
1516
|
+
|
|
1517
|
+
verify_path = task_dir / "verify.md"
|
|
1518
|
+
guard = validate_archive(task_dir, task_data)
|
|
1519
|
+
|
|
1520
|
+
if verify_path.is_file():
|
|
1521
|
+
try:
|
|
1522
|
+
existing = verify_path.read_text(encoding="utf-8")
|
|
1523
|
+
except OSError:
|
|
1524
|
+
return False, ["verify.md could not be read"]
|
|
1525
|
+
if ARCHIVE_EVIDENCE_DRAFT_MARKER in existing:
|
|
1526
|
+
messages.append(
|
|
1527
|
+
"verify.md already contains an archive evidence draft block; "
|
|
1528
|
+
"edit it in place instead of re-running prepare."
|
|
1529
|
+
)
|
|
1530
|
+
still_missing = _verify_evidence_errors(task_dir, task_data)
|
|
1531
|
+
if not still_missing:
|
|
1532
|
+
return False, messages
|
|
1533
|
+
messages.append(
|
|
1534
|
+
"Some evidence is still missing after the draft block; "
|
|
1535
|
+
"fill placeholders or add lines outside the draft section."
|
|
1536
|
+
)
|
|
1537
|
+
return False, messages
|
|
1538
|
+
content = existing
|
|
1539
|
+
else:
|
|
1540
|
+
content = "# Verification Evidence\n"
|
|
1541
|
+
|
|
1542
|
+
draft = build_archive_evidence_draft(task_dir, task_data, guard)
|
|
1543
|
+
status = _verify_evidence_status(task_dir, task_data)
|
|
1544
|
+
child_names = task_data.get("children")
|
|
1545
|
+
has_children = isinstance(child_names, list) and bool(child_names)
|
|
1546
|
+
needs_draft = (
|
|
1547
|
+
not status["validation"]
|
|
1548
|
+
or not status["acceptance"]
|
|
1549
|
+
or not status["durable_learning"]
|
|
1550
|
+
or (has_children and not status["integration"])
|
|
1551
|
+
or (guard.is_full_task and guard.required_gates)
|
|
1552
|
+
)
|
|
1553
|
+
if not needs_draft:
|
|
1554
|
+
messages.append("No missing archive evidence sections to draft.")
|
|
1555
|
+
return False, messages
|
|
1556
|
+
|
|
1557
|
+
new_content = content.rstrip() + "\n" + draft
|
|
1558
|
+
if dry_run:
|
|
1559
|
+
messages.append("Dry run: would append archive evidence draft to verify.md")
|
|
1560
|
+
return True, messages
|
|
1561
|
+
|
|
1562
|
+
try:
|
|
1563
|
+
verify_path.write_text(new_content, encoding="utf-8")
|
|
1564
|
+
except OSError as exc:
|
|
1565
|
+
return False, [f"failed to write verify.md: {exc}"]
|
|
1566
|
+
|
|
1567
|
+
messages.append("Appended archive evidence draft to verify.md")
|
|
1568
|
+
remaining = _verify_evidence_errors(task_dir, task_data)
|
|
1569
|
+
gate_errors = [
|
|
1570
|
+
e
|
|
1571
|
+
for e in validate_archive(task_dir, task_data).errors
|
|
1572
|
+
if e.startswith("missing gate record:")
|
|
1573
|
+
or e.startswith("gate failed:")
|
|
1574
|
+
or e.startswith("stale ")
|
|
1575
|
+
]
|
|
1576
|
+
if remaining:
|
|
1577
|
+
messages.append(
|
|
1578
|
+
"Still missing after draft (edit placeholders): " + "; ".join(remaining)
|
|
1579
|
+
)
|
|
1580
|
+
if gate_errors:
|
|
1581
|
+
messages.append(
|
|
1582
|
+
"Completion gates still require explicit record-gate: "
|
|
1583
|
+
+ "; ".join(gate_errors)
|
|
1584
|
+
)
|
|
1585
|
+
return True, messages
|
|
1586
|
+
|
|
1587
|
+
|
|
1588
|
+
def validate_reviewer_gate_input(
|
|
1589
|
+
transition: str,
|
|
1590
|
+
gate: str,
|
|
1591
|
+
result: str,
|
|
1592
|
+
reviewer: str,
|
|
1593
|
+
evidence: str,
|
|
1594
|
+
issue_fingerprint: str | None = None,
|
|
1595
|
+
issue_summary: str | None = None,
|
|
1596
|
+
root_cause: str | None = None,
|
|
1597
|
+
skip_approved_by: str | None = None,
|
|
1598
|
+
skip_reason: str | None = None,
|
|
1599
|
+
) -> list[str]:
|
|
1600
|
+
"""Validate CLI inputs for record-gate."""
|
|
1601
|
+
errors: list[str] = []
|
|
1602
|
+
|
|
1603
|
+
if transition not in KNOWN_TRANSITIONS:
|
|
1604
|
+
errors.append(f"unknown transition: {transition}")
|
|
1605
|
+
if gate == BASELINE_GATE:
|
|
1606
|
+
errors.append("baseline-check is CLI-owned and cannot be recorded manually")
|
|
1607
|
+
elif gate not in REVIEW_GATES:
|
|
1608
|
+
errors.append(f"unknown gate: {gate}")
|
|
1609
|
+
if result not in RESULTS:
|
|
1610
|
+
errors.append(f"invalid result: {result}")
|
|
1611
|
+
if not _is_short_token(reviewer) or not REVIEWER_RE.match(reviewer):
|
|
1612
|
+
errors.append("reviewer must be a short identifier")
|
|
1613
|
+
if not _is_short_text(evidence):
|
|
1614
|
+
errors.append("evidence must be a short reference, not a review body")
|
|
1615
|
+
|
|
1616
|
+
if result == "PASS":
|
|
1617
|
+
if issue_fingerprint:
|
|
1618
|
+
errors.append("--issue-fingerprint is only valid for FAIL")
|
|
1619
|
+
if root_cause:
|
|
1620
|
+
errors.append("--root-cause is only valid for FAIL")
|
|
1621
|
+
if skip_approved_by or skip_reason:
|
|
1622
|
+
errors.append("skip metadata is only valid for SKIPPED")
|
|
1623
|
+
elif result == "FAIL":
|
|
1624
|
+
if not _is_short_token(issue_fingerprint):
|
|
1625
|
+
errors.append("FAIL requires --issue-fingerprint")
|
|
1626
|
+
if root_cause not in FAIL_ROOT_CAUSES:
|
|
1627
|
+
errors.append(
|
|
1628
|
+
"FAIL requires --root-cause implementation-defect|contract-changing-defect|validation-environment-blocker"
|
|
1629
|
+
)
|
|
1630
|
+
if issue_summary and not _is_short_text(issue_summary, MAX_REASON):
|
|
1631
|
+
errors.append("--issue-summary must be short and single-line")
|
|
1632
|
+
if skip_approved_by or skip_reason:
|
|
1633
|
+
errors.append("skip metadata is only valid for SKIPPED")
|
|
1634
|
+
elif result == "SKIPPED":
|
|
1635
|
+
if issue_fingerprint or issue_summary or root_cause:
|
|
1636
|
+
errors.append("issue metadata is only valid for FAIL")
|
|
1637
|
+
if skip_approved_by != "user":
|
|
1638
|
+
errors.append("SKIPPED requires --skip-approved-by user")
|
|
1639
|
+
if not _is_short_text(skip_reason, MAX_REASON):
|
|
1640
|
+
errors.append("SKIPPED requires --skip-reason")
|
|
1641
|
+
|
|
1642
|
+
return errors
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
def _route_for_fail_root_cause(root_cause: str | None) -> str | None:
|
|
1646
|
+
if root_cause is None:
|
|
1647
|
+
return None
|
|
1648
|
+
return FAIL_ROOT_CAUSES.get(root_cause)
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
def _validate_quality_gates(
|
|
1652
|
+
gates: dict,
|
|
1653
|
+
verification_profile: str | None,
|
|
1654
|
+
) -> list[str]:
|
|
1655
|
+
errors: list[str] = []
|
|
1656
|
+
mode = gates.get("mode")
|
|
1657
|
+
if mode not in ALLOWED_GATE_MODES:
|
|
1658
|
+
errors.append("quality_gates.mode must be profile or explicit")
|
|
1659
|
+
|
|
1660
|
+
profile = gates.get("profile")
|
|
1661
|
+
if profile and profile not in ALLOWED_VERIFICATION_PROFILES:
|
|
1662
|
+
errors.append(f"invalid quality_gates.profile: {profile}")
|
|
1663
|
+
|
|
1664
|
+
enabled = gates.get("enabled", [])
|
|
1665
|
+
disabled = gates.get("disabled", [])
|
|
1666
|
+
if enabled is None:
|
|
1667
|
+
enabled = []
|
|
1668
|
+
if disabled is None:
|
|
1669
|
+
disabled = []
|
|
1670
|
+
|
|
1671
|
+
if mode == "explicit":
|
|
1672
|
+
if not isinstance(enabled, list):
|
|
1673
|
+
errors.append("quality_gates.enabled must be a list in explicit mode")
|
|
1674
|
+
if not isinstance(disabled, list):
|
|
1675
|
+
errors.append("quality_gates.disabled must be a list in explicit mode")
|
|
1676
|
+
|
|
1677
|
+
all_configured = []
|
|
1678
|
+
if isinstance(enabled, list):
|
|
1679
|
+
all_configured.extend(enabled)
|
|
1680
|
+
if isinstance(disabled, list):
|
|
1681
|
+
all_configured.extend(disabled)
|
|
1682
|
+
|
|
1683
|
+
for gate in all_configured:
|
|
1684
|
+
if gate not in KNOWN_GATES:
|
|
1685
|
+
errors.append(f"unknown quality gate: {gate}")
|
|
1686
|
+
|
|
1687
|
+
if BASELINE_GATE in disabled:
|
|
1688
|
+
errors.append("baseline-check cannot be disabled")
|
|
1689
|
+
|
|
1690
|
+
active = set(enabled) if mode == "explicit" else set(
|
|
1691
|
+
PROFILE_DEFAULT_GATES.get(
|
|
1692
|
+
profile or verification_profile or "standard",
|
|
1693
|
+
PROFILE_DEFAULT_GATES["standard"],
|
|
1694
|
+
)
|
|
1695
|
+
)
|
|
1696
|
+
if "architecture-deep-review" in active and "architecture-review" not in active:
|
|
1697
|
+
errors.append("architecture-deep-review requires architecture-review")
|
|
1698
|
+
|
|
1699
|
+
return errors
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def _parse_inline_list(value: str) -> list[str] | str:
|
|
1703
|
+
value = value.strip()
|
|
1704
|
+
if value.startswith("[") and value.endswith("]"):
|
|
1705
|
+
inner = value[1:-1].strip()
|
|
1706
|
+
if not inner:
|
|
1707
|
+
return []
|
|
1708
|
+
return [
|
|
1709
|
+
item.strip().strip("'\"")
|
|
1710
|
+
for item in inner.split(",")
|
|
1711
|
+
if item.strip()
|
|
1712
|
+
]
|
|
1713
|
+
return value
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
def _hash_payload(payload: dict) -> str:
|
|
1717
|
+
body = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
|
|
1718
|
+
digest = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
|
1719
|
+
return f"sha256:{digest}"
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
def _stable_task_data(task_data: dict) -> dict:
|
|
1723
|
+
stable: dict = {}
|
|
1724
|
+
for key in STABLE_TASK_KEYS:
|
|
1725
|
+
if key in task_data:
|
|
1726
|
+
stable[key] = task_data.get(key)
|
|
1727
|
+
return stable
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _parent_contract_metadata(
|
|
1731
|
+
task_dir: Path,
|
|
1732
|
+
task_data: dict,
|
|
1733
|
+
transition: str,
|
|
1734
|
+
) -> dict | None:
|
|
1735
|
+
if transition == "child-review":
|
|
1736
|
+
parent_name = task_data.get("parent")
|
|
1737
|
+
if not isinstance(parent_name, str) or not parent_name:
|
|
1738
|
+
return None
|
|
1739
|
+
parent_dir = task_dir.parent / parent_name
|
|
1740
|
+
data, _ = load_task_map(parent_dir)
|
|
1741
|
+
return _task_map_contract_metadata("child", parent_dir, data)
|
|
1742
|
+
|
|
1743
|
+
if transition.startswith("parent-"):
|
|
1744
|
+
data, _ = load_task_map(task_dir)
|
|
1745
|
+
return _task_map_contract_metadata("parent", task_dir, data)
|
|
1746
|
+
|
|
1747
|
+
return None
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def _task_map_contract_metadata(
|
|
1751
|
+
role: str,
|
|
1752
|
+
parent_dir: Path,
|
|
1753
|
+
task_map_data: dict | None,
|
|
1754
|
+
) -> dict:
|
|
1755
|
+
return {
|
|
1756
|
+
"role": role,
|
|
1757
|
+
"parent": parent_dir.name,
|
|
1758
|
+
"task_map_found": task_map_data is not None,
|
|
1759
|
+
"contract_epoch": (
|
|
1760
|
+
task_map_data.get("contract_epoch")
|
|
1761
|
+
if isinstance(task_map_data, dict)
|
|
1762
|
+
else None
|
|
1763
|
+
),
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def _reviewed_change_set_metadata(
|
|
1768
|
+
task_dir: Path,
|
|
1769
|
+
transition: str,
|
|
1770
|
+
) -> dict | None:
|
|
1771
|
+
evidence_files = _change_set_evidence_files_for(transition)
|
|
1772
|
+
if not evidence_files:
|
|
1773
|
+
return None
|
|
1774
|
+
|
|
1775
|
+
files = []
|
|
1776
|
+
for name in evidence_files:
|
|
1777
|
+
path = task_dir / name
|
|
1778
|
+
try:
|
|
1779
|
+
content = path.read_text(encoding="utf-8") if path.is_file() else ""
|
|
1780
|
+
except OSError:
|
|
1781
|
+
content = ""
|
|
1782
|
+
files.append(
|
|
1783
|
+
{
|
|
1784
|
+
"path": name,
|
|
1785
|
+
"present": path.is_file(),
|
|
1786
|
+
"entries": _extract_reviewed_change_set_entries(content),
|
|
1787
|
+
}
|
|
1788
|
+
)
|
|
1789
|
+
return {"files": files}
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
def _change_set_evidence_files_for(transition: str) -> list[str]:
|
|
1793
|
+
if transition == "full-task-complete":
|
|
1794
|
+
return ["verify.md"]
|
|
1795
|
+
if transition == "child-review":
|
|
1796
|
+
return ["verify.md", "handoff.md"]
|
|
1797
|
+
if transition.startswith("parent-"):
|
|
1798
|
+
return ["task-map.md", "verify.md"]
|
|
1799
|
+
return []
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def _extract_reviewed_change_set_entries(content: str) -> list[str]:
|
|
1803
|
+
return [
|
|
1804
|
+
match.group(1).strip()
|
|
1805
|
+
for match in REVIEWED_CHANGE_SET_RE.finditer(content)
|
|
1806
|
+
]
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
def _only_missing_gate_record(errors: list[str]) -> bool:
|
|
1810
|
+
return bool(errors) and all(
|
|
1811
|
+
error.startswith("missing gate record:") for error in errors
|
|
1812
|
+
)
|
|
1813
|
+
|
|
1814
|
+
|
|
1815
|
+
def make_planning_review_gate_record(
|
|
1816
|
+
task_dir: Path,
|
|
1817
|
+
task_data: dict,
|
|
1818
|
+
transition: str,
|
|
1819
|
+
gate: str,
|
|
1820
|
+
contract: dict,
|
|
1821
|
+
contract_fingerprint: str,
|
|
1822
|
+
artifact_fingerprint: str,
|
|
1823
|
+
) -> dict:
|
|
1824
|
+
"""Build a CLI-owned PASS record when planning artifacts satisfy the gate."""
|
|
1825
|
+
_ = task_dir, task_data, contract
|
|
1826
|
+
evidence_by_gate = {
|
|
1827
|
+
"requirements-review": "prd.md",
|
|
1828
|
+
"architecture-review": "design.md+implement.md",
|
|
1829
|
+
}
|
|
1830
|
+
return {
|
|
1831
|
+
"schema_version": SCHEMA_VERSION,
|
|
1832
|
+
"transition": transition,
|
|
1833
|
+
"gate": gate,
|
|
1834
|
+
"result": "PASS",
|
|
1835
|
+
"reviewer": AUTO_PLANNING_REVIEWER,
|
|
1836
|
+
"evidence": evidence_by_gate.get(gate, "planning-artifacts"),
|
|
1837
|
+
"checked_at": utc_now(),
|
|
1838
|
+
"contract_fingerprint": contract_fingerprint,
|
|
1839
|
+
"artifact_fingerprint": artifact_fingerprint,
|
|
1840
|
+
"issue_fingerprint": None,
|
|
1841
|
+
"root_cause": None,
|
|
1842
|
+
"route": None,
|
|
1843
|
+
"consecutive_failures": 0,
|
|
1844
|
+
"required_user_choice": None,
|
|
1845
|
+
"approved_skip": None,
|
|
1846
|
+
"auto_recorded": True,
|
|
1847
|
+
"auto_record_reason": "planning-artifacts",
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
def _start_execution_planning_gate_readiness_errors(
|
|
1852
|
+
task_dir: Path,
|
|
1853
|
+
gate: str,
|
|
1854
|
+
*,
|
|
1855
|
+
full_task: bool,
|
|
1856
|
+
contract: dict,
|
|
1857
|
+
) -> list[str]:
|
|
1858
|
+
if gate == "requirements-review":
|
|
1859
|
+
return _prd_requirements_review_errors(task_dir)
|
|
1860
|
+
if gate == "architecture-review":
|
|
1861
|
+
errors = _required_file_errors(task_dir, ["design.md", "implement.md"])
|
|
1862
|
+
if not full_task:
|
|
1863
|
+
errors.append(
|
|
1864
|
+
"architecture-review requires design.md and implement.md (Full Task)"
|
|
1865
|
+
)
|
|
1866
|
+
errors.extend(validate_strategy_contract(contract))
|
|
1867
|
+
return errors
|
|
1868
|
+
return [f"planning gate readiness check unsupported: {gate}"]
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
def _prd_requirements_review_errors(task_dir: Path) -> list[str]:
|
|
1872
|
+
path = task_dir / "prd.md"
|
|
1873
|
+
if not path.is_file():
|
|
1874
|
+
return ["prd.md missing for requirements-review"]
|
|
1875
|
+
try:
|
|
1876
|
+
content = path.read_text(encoding="utf-8")
|
|
1877
|
+
except OSError:
|
|
1878
|
+
return ["prd.md could not be read for requirements-review"]
|
|
1879
|
+
if not content.strip():
|
|
1880
|
+
return ["prd.md is empty (requirements-review)"]
|
|
1881
|
+
if not PRD_ACCEPTANCE_CRITERIA_HEADING_RE.search(content):
|
|
1882
|
+
return [
|
|
1883
|
+
"prd.md missing Acceptance Criteria heading (complete PRD Grill before start-execution)"
|
|
1884
|
+
]
|
|
1885
|
+
criteria: list[str] = []
|
|
1886
|
+
for match in PRD_ACCEPTANCE_ITEM_RE.finditer(content):
|
|
1887
|
+
text = match.group(1).strip()
|
|
1888
|
+
if text and not PRD_PLACEHOLDER_RE.match(text):
|
|
1889
|
+
criteria.append(text)
|
|
1890
|
+
if not criteria:
|
|
1891
|
+
return [
|
|
1892
|
+
"prd.md needs at least one non-placeholder Acceptance Criteria item (replace TBD checkboxes)"
|
|
1893
|
+
]
|
|
1894
|
+
return []
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
def start_execution_repair_hints(
|
|
1898
|
+
errors: list[str],
|
|
1899
|
+
task_dir: Path,
|
|
1900
|
+
) -> list[str]:
|
|
1901
|
+
"""Map start-execution validation errors to actionable planning hints."""
|
|
1902
|
+
hints: list[str] = []
|
|
1903
|
+
rel_task = f".cstl/tasks/{task_dir.name}"
|
|
1904
|
+
for error in errors:
|
|
1905
|
+
if error.startswith("missing gate record:"):
|
|
1906
|
+
_rest = error.split(":", 1)[1].strip()
|
|
1907
|
+
_transition, gate = _rest.split("/", 1)
|
|
1908
|
+
if gate in START_EXECUTION_AUTO_GATES:
|
|
1909
|
+
hints.append(
|
|
1910
|
+
"Complete planning artifacts so Trellis can auto-record this gate on "
|
|
1911
|
+
f"`start-execution --approved` (requirements: prd.md Acceptance Criteria; "
|
|
1912
|
+
"architecture: design.md + valid implement.md contract)."
|
|
1913
|
+
)
|
|
1914
|
+
else:
|
|
1915
|
+
hints.append(
|
|
1916
|
+
f"python ./.cstl/scripts/task.py record-gate {rel_task} "
|
|
1917
|
+
f"--transition start-execution --gate {gate} --result PASS "
|
|
1918
|
+
f"--reviewer <reviewer-id> --evidence verify.md"
|
|
1919
|
+
)
|
|
1920
|
+
continue
|
|
1921
|
+
if "Acceptance Criteria" in error or "TBD" in error:
|
|
1922
|
+
hints.append(
|
|
1923
|
+
"Finish PRD Grill in cstl-brainstorm: fill Goal, Acceptance Criteria "
|
|
1924
|
+
"(non-TBD checkboxes), then re-run start-execution --check."
|
|
1925
|
+
)
|
|
1926
|
+
continue
|
|
1927
|
+
if "Development Strategy Contract" in error or error.startswith("invalid "):
|
|
1928
|
+
hints.append(
|
|
1929
|
+
"Fix the Development Strategy Contract block in implement.md "
|
|
1930
|
+
"(execution_mode, isolation, verification_profile, quality_gates)."
|
|
1931
|
+
)
|
|
1932
|
+
continue
|
|
1933
|
+
if error == "design.md and implement.md must be present together for Full Tasks":
|
|
1934
|
+
hints.append(
|
|
1935
|
+
"Add both design.md and implement.md for Full Tasks, or stay PRD-only for Lite."
|
|
1936
|
+
)
|
|
1937
|
+
return _dedupe_preserve_order(hints)
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
def _artifact_files_for(transition: str, gate: str) -> list[str]:
|
|
1941
|
+
if transition == "start-execution":
|
|
1942
|
+
return ["prd.md", "design.md", "implement.md"]
|
|
1943
|
+
if transition == "full-task-complete":
|
|
1944
|
+
return ["prd.md", "design.md", "implement.md", "verify.md"]
|
|
1945
|
+
if transition == "child-review":
|
|
1946
|
+
return ["prd.md", "design.md", "implement.md", "verify.md", "handoff.md"]
|
|
1947
|
+
if transition in ("parent-accepted", "parent-integrated"):
|
|
1948
|
+
return ["task-map.md", "verify.md"]
|
|
1949
|
+
_ = gate
|
|
1950
|
+
return ["prd.md", "verify.md"]
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def _read_artifact_files(task_dir: Path, names: list[str]) -> list[dict]:
|
|
1954
|
+
files = []
|
|
1955
|
+
for name in names:
|
|
1956
|
+
path = task_dir / name
|
|
1957
|
+
try:
|
|
1958
|
+
content = path.read_text(encoding="utf-8") if path.is_file() else None
|
|
1959
|
+
except OSError:
|
|
1960
|
+
content = None
|
|
1961
|
+
files.append({"path": name, "content": content})
|
|
1962
|
+
return files
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
def _required_file_errors(task_dir: Path, names: list[str]) -> list[str]:
|
|
1966
|
+
errors = []
|
|
1967
|
+
for name in names:
|
|
1968
|
+
path = task_dir / name
|
|
1969
|
+
if not path.is_file():
|
|
1970
|
+
errors.append(name)
|
|
1971
|
+
continue
|
|
1972
|
+
try:
|
|
1973
|
+
if not path.read_text(encoding="utf-8").strip():
|
|
1974
|
+
errors.append(f"{name} is empty")
|
|
1975
|
+
except OSError:
|
|
1976
|
+
errors.append(f"{name} could not be read")
|
|
1977
|
+
return errors
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
def _get_gate_record(task_data: dict, transition: str, gate: str) -> dict | None:
|
|
1981
|
+
qgr = task_data.get("quality_gate_results")
|
|
1982
|
+
if not isinstance(qgr, dict):
|
|
1983
|
+
return None
|
|
1984
|
+
transitions = qgr.get("transitions")
|
|
1985
|
+
if not isinstance(transitions, dict):
|
|
1986
|
+
return None
|
|
1987
|
+
transition_records = transitions.get(transition)
|
|
1988
|
+
if not isinstance(transition_records, dict):
|
|
1989
|
+
return None
|
|
1990
|
+
record = transition_records.get(gate)
|
|
1991
|
+
return record if isinstance(record, dict) else None
|
|
1992
|
+
|
|
1993
|
+
|
|
1994
|
+
def _validate_gate_record_for_transition(
|
|
1995
|
+
task_data: dict,
|
|
1996
|
+
transition: str,
|
|
1997
|
+
gate: str,
|
|
1998
|
+
contract_fingerprint: str,
|
|
1999
|
+
artifact_fingerprint: str,
|
|
2000
|
+
) -> list[str]:
|
|
2001
|
+
errors: list[str] = []
|
|
2002
|
+
record = _get_gate_record(task_data, transition, gate)
|
|
2003
|
+
if record is None:
|
|
2004
|
+
errors.append(f"missing gate record: {transition}/{gate}")
|
|
2005
|
+
return errors
|
|
2006
|
+
|
|
2007
|
+
result = record.get("result")
|
|
2008
|
+
if result == "FAIL":
|
|
2009
|
+
errors.append(f"gate failed: {transition}/{gate}")
|
|
2010
|
+
return errors
|
|
2011
|
+
if result not in ("PASS", "SKIPPED"):
|
|
2012
|
+
errors.append(f"gate record has invalid result: {transition}/{gate}")
|
|
2013
|
+
return errors
|
|
2014
|
+
if record.get("contract_fingerprint") != contract_fingerprint:
|
|
2015
|
+
errors.append(f"stale contract fingerprint: {transition}/{gate}")
|
|
2016
|
+
if record.get("artifact_fingerprint") != artifact_fingerprint:
|
|
2017
|
+
errors.append(f"stale artifact fingerprint: {transition}/{gate}")
|
|
2018
|
+
if result == "SKIPPED":
|
|
2019
|
+
approved_skip = record.get("approved_skip")
|
|
2020
|
+
if not isinstance(approved_skip, dict):
|
|
2021
|
+
errors.append(f"SKIPPED gate lacks approval metadata: {transition}/{gate}")
|
|
2022
|
+
elif approved_skip.get("approved_by") != "user" or not approved_skip.get("reason"):
|
|
2023
|
+
errors.append(f"invalid SKIPPED approval metadata: {transition}/{gate}")
|
|
2024
|
+
return errors
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
def _next_consecutive_failures(
|
|
2028
|
+
previous: dict | None,
|
|
2029
|
+
issue_fingerprint: str | None,
|
|
2030
|
+
) -> int:
|
|
2031
|
+
if not previous or previous.get("result") != "FAIL":
|
|
2032
|
+
return 1
|
|
2033
|
+
if previous.get("issue_fingerprint") != issue_fingerprint:
|
|
2034
|
+
return 1
|
|
2035
|
+
previous_count = previous.get("consecutive_failures")
|
|
2036
|
+
return int(previous_count) + 1 if isinstance(previous_count, int) else 2
|
|
2037
|
+
|
|
2038
|
+
|
|
2039
|
+
def _is_short_token(value: str | None, max_len: int = MAX_SHORT_FIELD) -> bool:
|
|
2040
|
+
if not value or not isinstance(value, str):
|
|
2041
|
+
return False
|
|
2042
|
+
if len(value) > max_len:
|
|
2043
|
+
return False
|
|
2044
|
+
if any(ch.isspace() for ch in value):
|
|
2045
|
+
return False
|
|
2046
|
+
return True
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def _is_short_text(value: str | None, max_len: int = MAX_SHORT_FIELD) -> bool:
|
|
2050
|
+
if not value or not isinstance(value, str):
|
|
2051
|
+
return False
|
|
2052
|
+
if len(value) > max_len:
|
|
2053
|
+
return False
|
|
2054
|
+
if "\n" in value or "\r" in value:
|
|
2055
|
+
return False
|
|
2056
|
+
return True
|