@hunter-harness/workflow-harness 0.2.7 → 0.2.9
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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +3 -3
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-archive/reference.md +18 -10
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +5 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +5 -0
- package/harness/bundles/general/claude-code/harness-run/reference.md +31 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/checklist.md +9 -0
- package/harness/bundles/general/claude-code/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +36 -0
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +568 -58
- package/harness/bundles/general/claude-code/scripts/harness_change.py +20 -2
- package/harness/bundles/general/claude-code/scripts/harness_events.py +96 -19
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +178 -25
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +17 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +3 -3
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +18 -10
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +5 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +5 -0
- package/harness/bundles/general/codebuddy/harness-run/reference.md +31 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +9 -0
- package/harness/bundles/general/codebuddy/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +36 -0
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +568 -58
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +20 -2
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +96 -19
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +178 -25
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +17 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +3 -3
- package/harness/bundles/general/codex/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-archive/reference.md +18 -10
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +5 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +5 -0
- package/harness/bundles/general/codex/harness-run/reference.md +31 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/checklist.md +9 -0
- package/harness/bundles/general/codex/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/codex/harness-test/reference.md +36 -0
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/general/codex/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +568 -58
- package/harness/bundles/general/codex/scripts/harness_change.py +20 -2
- package/harness/bundles/general/codex/scripts/harness_events.py +96 -19
- package/harness/bundles/general/codex/scripts/harness_gate.py +178 -25
- package/harness/bundles/general/codex/scripts/harness_ledger.py +17 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +3 -3
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-archive/reference.md +18 -10
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +5 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +5 -0
- package/harness/bundles/general/cursor/harness-run/reference.md +31 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/checklist.md +9 -0
- package/harness/bundles/general/cursor/harness-test/pitfalls-java.md +14 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +36 -0
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/general/cursor/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +568 -58
- package/harness/bundles/general/cursor/scripts/harness_change.py +20 -2
- package/harness/bundles/general/cursor/scripts/harness_events.py +96 -19
- package/harness/bundles/general/cursor/scripts/harness_gate.py +178 -25
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +17 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +3 -3
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-archive/reference.md +18 -10
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +5 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +568 -58
- package/harness/bundles/java/claude-code/scripts/harness_change.py +20 -2
- package/harness/bundles/java/claude-code/scripts/harness_events.py +96 -19
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +178 -25
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +17 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +3 -3
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +18 -10
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +5 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +568 -58
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +20 -2
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +96 -19
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +178 -25
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +17 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +3 -3
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-archive/reference.md +18 -10
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +5 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/java/codex/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +568 -58
- package/harness/bundles/java/codex/scripts/harness_change.py +20 -2
- package/harness/bundles/java/codex/scripts/harness_events.py +96 -19
- package/harness/bundles/java/codex/scripts/harness_gate.py +178 -25
- package/harness/bundles/java/codex/scripts/harness_ledger.py +17 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +3 -3
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-archive/reference.md +18 -10
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +5 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +3 -1
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +2 -0
- package/harness/bundles/java/cursor/protocols/powershell-protocol.md +16 -0
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +17 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +568 -58
- package/harness/bundles/java/cursor/scripts/harness_change.py +20 -2
- package/harness/bundles/java/cursor/scripts/harness_events.py +96 -19
- package/harness/bundles/java/cursor/scripts/harness_gate.py +178 -25
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +17 -0
- package/harness/manifests/general/claude-code.json +33 -29
- package/harness/manifests/general/codebuddy.json +33 -29
- package/harness/manifests/general/codex.json +33 -29
- package/harness/manifests/general/cursor.json +33 -29
- package/harness/manifests/java/claude-code.json +27 -27
- package/harness/manifests/java/codebuddy.json +27 -27
- package/harness/manifests/java/codex.json +27 -27
- package/harness/manifests/java/cursor.json +27 -27
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -172,12 +172,30 @@ def list_active_changes(project_root: Path) -> list[dict[str, Any]]:
|
|
|
172
172
|
archived = {}
|
|
173
173
|
if isinstance(archived, dict) and archived.get("status") == "archived":
|
|
174
174
|
continue
|
|
175
|
+
has_plan = any((entry / "plans").glob("*-plan.md"))
|
|
176
|
+
has_context = (entry / CHANGE_CONTEXT_REL).is_file()
|
|
177
|
+
has_checkpoints = (entry / CHECKPOINTS_REL).is_file()
|
|
178
|
+
worktree_path = entry / WORKTREE_META_REL
|
|
179
|
+
has_active_worktree = False
|
|
180
|
+
if worktree_path.is_file():
|
|
181
|
+
try:
|
|
182
|
+
worktree = _read_json(worktree_path)
|
|
183
|
+
has_active_worktree = isinstance(worktree, dict) and (
|
|
184
|
+
worktree.get("created") is True or
|
|
185
|
+
worktree.get("requested") is True
|
|
186
|
+
)
|
|
187
|
+
except (OSError, json.JSONDecodeError):
|
|
188
|
+
has_active_worktree = False
|
|
189
|
+
# Runtime notes, event logs and requested=false worktree metadata can
|
|
190
|
+
# survive archive/submit cleanup. They are residues, not active changes.
|
|
191
|
+
if not (has_plan or has_context or has_checkpoints or has_active_worktree):
|
|
192
|
+
continue
|
|
175
193
|
active.append(
|
|
176
194
|
{
|
|
177
195
|
"changeId": entry.name,
|
|
178
196
|
"path": str(entry.resolve()),
|
|
179
|
-
"hasPlan":
|
|
180
|
-
"hasWorktreeMeta":
|
|
197
|
+
"hasPlan": has_plan,
|
|
198
|
+
"hasWorktreeMeta": worktree_path.is_file(),
|
|
181
199
|
}
|
|
182
200
|
)
|
|
183
201
|
return active
|
|
@@ -43,6 +43,7 @@ EVENT_TYPES = frozenset(
|
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
SCHEMA_VERSION = 3
|
|
46
|
+
_NOTE_FALLBACK_MAXLEN = 60
|
|
46
47
|
HEADER_LINE = (
|
|
47
48
|
"本文件由 harness_events.py 自动渲染,请勿手工编辑;事实源为 events.ndjson"
|
|
48
49
|
)
|
|
@@ -106,6 +107,25 @@ def execution_log_path(change_dir: Path) -> Path:
|
|
|
106
107
|
return change_dir / "logs" / "execution-log.md"
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
def archived_change_dir(change_dir: Path) -> Path | None:
|
|
111
|
+
"""Return the matching archive when change_dir follows the project layout."""
|
|
112
|
+
changes_root = change_dir.parent
|
|
113
|
+
harness_root = changes_root.parent
|
|
114
|
+
if changes_root.name != "changes" or harness_root.name != ".harness":
|
|
115
|
+
return None
|
|
116
|
+
archive_root = harness_root / "archive"
|
|
117
|
+
if not archive_root.is_dir():
|
|
118
|
+
return None
|
|
119
|
+
suffix = "-" + change_dir.name
|
|
120
|
+
matches = sorted(
|
|
121
|
+
candidate for candidate in archive_root.iterdir()
|
|
122
|
+
if candidate.is_dir() and (
|
|
123
|
+
candidate.name == change_dir.name or candidate.name.endswith(suffix)
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return matches[-1] if matches else None
|
|
127
|
+
|
|
128
|
+
|
|
109
129
|
def parse_timestamp(value: Any) -> dt.datetime | None:
|
|
110
130
|
if not isinstance(value, str) or not value.strip():
|
|
111
131
|
return None
|
|
@@ -297,6 +317,8 @@ def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dic
|
|
|
297
317
|
def status_symbol(status: Any, reason: Any = None) -> str:
|
|
298
318
|
text = str(status or "").strip().lower()
|
|
299
319
|
reason_text = str(reason or "").strip()
|
|
320
|
+
if not text:
|
|
321
|
+
return "—"
|
|
300
322
|
ok_set = {"ok", "passed", "pass", "success", "green", "✅", "✅ok"}
|
|
301
323
|
warn_set = {"warn", "warning", "yellow", "skipped", "skip", "🟡", "🟡warn"}
|
|
302
324
|
fail_set = {"fail", "failed", "error", "red", "blocked", "❌", "❌fail"}
|
|
@@ -308,7 +330,7 @@ def status_symbol(status: Any, reason: Any = None) -> str:
|
|
|
308
330
|
return f"❌FAIL({reason_text})" if reason_text else "❌FAIL"
|
|
309
331
|
if reason_text:
|
|
310
332
|
return f"{status}({reason_text})"
|
|
311
|
-
return str(status
|
|
333
|
+
return str(status)
|
|
312
334
|
|
|
313
335
|
|
|
314
336
|
def severity_symbol(severity: Any, message: Any = None) -> str:
|
|
@@ -320,7 +342,10 @@ def severity_symbol(severity: Any, message: Any = None) -> str:
|
|
|
320
342
|
return f"🟡WARN({msg})" if msg else "🟡WARN"
|
|
321
343
|
if text in {"info", "ok", "note"}:
|
|
322
344
|
return f"✅OK({msg})" if msg else "✅OK"
|
|
323
|
-
|
|
345
|
+
# Empty/unknown severity: never emit literal "issue"/"None".
|
|
346
|
+
if not text:
|
|
347
|
+
return ""
|
|
348
|
+
return f"{severity}: {msg}" if msg else str(severity)
|
|
324
349
|
|
|
325
350
|
|
|
326
351
|
def format_duration(ms: int | None) -> str:
|
|
@@ -421,35 +446,54 @@ def phase_duration_ms(phase_events: list[dict[str, Any]]) -> int | None:
|
|
|
421
446
|
def render_command_block(commands: list[dict[str, Any]]) -> list[str]:
|
|
422
447
|
if not commands:
|
|
423
448
|
return []
|
|
449
|
+
|
|
450
|
+
def command_display(event: dict[str, Any]) -> str:
|
|
451
|
+
cmd = str(event.get("command") or "").strip()
|
|
452
|
+
if cmd:
|
|
453
|
+
return cmd
|
|
454
|
+
return str(event.get("note") or "").strip()
|
|
455
|
+
|
|
424
456
|
if len(commands) == 1:
|
|
425
457
|
event = commands[0]
|
|
426
|
-
|
|
458
|
+
display = command_display(event)
|
|
459
|
+
if not display:
|
|
460
|
+
return []
|
|
427
461
|
exit_code = event.get("exit_code")
|
|
428
462
|
exit_text = "?" if exit_code is None else str(exit_code)
|
|
429
463
|
duration = format_duration(
|
|
430
464
|
int(event["duration_ms"]) if isinstance(event.get("duration_ms"), int) else None
|
|
431
465
|
)
|
|
432
466
|
note = str(event.get("note") or "").strip()
|
|
433
|
-
parts = [f"- command: `{
|
|
434
|
-
|
|
467
|
+
parts = [f"- command: `{display}`", f"exit={exit_text}", f"duration={duration}"]
|
|
468
|
+
# Avoid duplicating note when it was already used as the display text.
|
|
469
|
+
if note and note != display:
|
|
435
470
|
parts.append(f"note={note}")
|
|
436
471
|
return [" · ".join(parts)]
|
|
437
472
|
|
|
438
|
-
|
|
439
|
-
"",
|
|
440
|
-
"| 命令 | exit | duration | note |",
|
|
441
|
-
"| --- | ---: | ---: | --- |",
|
|
442
|
-
]
|
|
473
|
+
rows: list[str] = []
|
|
443
474
|
for event in commands:
|
|
444
|
-
|
|
475
|
+
display = command_display(event)
|
|
476
|
+
if not display:
|
|
477
|
+
continue
|
|
478
|
+
cmd = display.replace("|", "\\|")
|
|
445
479
|
exit_code = event.get("exit_code")
|
|
446
480
|
exit_text = "?" if exit_code is None else str(exit_code)
|
|
447
481
|
duration = format_duration(
|
|
448
482
|
int(event["duration_ms"]) if isinstance(event.get("duration_ms"), int) else None
|
|
449
483
|
)
|
|
450
484
|
note = str(event.get("note") or "").replace("|", "\\|")
|
|
451
|
-
|
|
452
|
-
|
|
485
|
+
# If note was promoted to the command cell, leave the note column empty.
|
|
486
|
+
note_cell = "" if note == display else note
|
|
487
|
+
rows.append(f"| `{cmd}` | {exit_text} | {duration} | {note_cell} |")
|
|
488
|
+
if not rows:
|
|
489
|
+
return []
|
|
490
|
+
lines = [
|
|
491
|
+
"",
|
|
492
|
+
"| 命令 | exit | duration | note |",
|
|
493
|
+
"| --- | ---: | ---: | --- |",
|
|
494
|
+
*rows,
|
|
495
|
+
"",
|
|
496
|
+
]
|
|
453
497
|
return lines
|
|
454
498
|
|
|
455
499
|
|
|
@@ -472,23 +516,37 @@ def render_event_line(event: dict[str, Any]) -> list[str]:
|
|
|
472
516
|
suffix = f" — {note}" if note else ""
|
|
473
517
|
return [f"- phase.end @ {event.get('timestamp', '')}{suffix}"]
|
|
474
518
|
if etype == "verification":
|
|
475
|
-
|
|
476
|
-
|
|
519
|
+
note = str(event.get("note") or "").strip()
|
|
520
|
+
name = str(event.get("name") or "").strip() or (
|
|
521
|
+
note[:_NOTE_FALLBACK_MAXLEN] if note else ""
|
|
522
|
+
)
|
|
523
|
+
if not name:
|
|
524
|
+
return []
|
|
525
|
+
symbol = status_symbol(event.get("status"), event.get("reason"))
|
|
477
526
|
return [f"- verification: {name} → {symbol}"]
|
|
478
527
|
if etype == "decision":
|
|
479
|
-
decision = event.get("decision") or ""
|
|
480
|
-
reason = event.get("reason") or event.get("note") or ""
|
|
528
|
+
decision = str(event.get("decision") or "").strip()
|
|
529
|
+
reason = str(event.get("reason") or event.get("note") or "").strip()
|
|
530
|
+
if not decision:
|
|
531
|
+
return [f"- decision: {reason}"] if reason else []
|
|
481
532
|
if reason:
|
|
482
533
|
return [f"- decision: {decision} — {reason}"]
|
|
483
534
|
return [f"- decision: {decision}"]
|
|
484
535
|
if etype == "issue":
|
|
485
|
-
|
|
536
|
+
note = str(event.get("note") or "").strip()
|
|
537
|
+
message = str(event.get("message") or "").strip() or note
|
|
538
|
+
symbol = severity_symbol(event.get("severity"), message)
|
|
539
|
+
if not symbol:
|
|
540
|
+
return [f"- issue: {note}"] if note else []
|
|
486
541
|
code = event.get("code")
|
|
487
542
|
prefix = f"[{code}] " if code else ""
|
|
488
543
|
return [f"- issue: {prefix}{symbol}"]
|
|
489
544
|
if etype == "artifact":
|
|
490
|
-
path = event.get("path") or ""
|
|
545
|
+
path = str(event.get("path") or "").strip()
|
|
546
|
+
note = str(event.get("note") or "").strip()
|
|
491
547
|
kind = event.get("kind")
|
|
548
|
+
if not path:
|
|
549
|
+
return [f"- artifact: {note}"] if note else []
|
|
492
550
|
if kind:
|
|
493
551
|
return [f"- artifact: `{path}` ({kind})"]
|
|
494
552
|
return [f"- artifact: `{path}`"]
|
|
@@ -620,6 +678,13 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
620
678
|
as_json=as_json,
|
|
621
679
|
)
|
|
622
680
|
change_dir = resolve_change_dir(args.change_dir)
|
|
681
|
+
archived = archived_change_dir(change_dir)
|
|
682
|
+
if archived is not None:
|
|
683
|
+
return emit_error(
|
|
684
|
+
"ARCHIVED_CHANGE_IMMUTABLE: refusing to append to archived change "
|
|
685
|
+
f"{change_dir.name} ({archived})",
|
|
686
|
+
as_json=as_json,
|
|
687
|
+
)
|
|
623
688
|
path = events_path(change_dir)
|
|
624
689
|
lock_path = change_dir / "events.ndjson.lock"
|
|
625
690
|
|
|
@@ -627,6 +692,18 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
627
692
|
# new_event_id 用完整 uuid,无需扫描去重。锁覆盖 atomic_append_line 的
|
|
628
693
|
# open/write/flush/fsync 全过程。
|
|
629
694
|
event = build_event(args, [])
|
|
695
|
+
# Semantic hints (non-fatal): guide agents toward required fields.
|
|
696
|
+
if args.type == "issue" and not args.severity:
|
|
697
|
+
event["severity"] = "info"
|
|
698
|
+
print(
|
|
699
|
+
"warning: issue without --severity, defaulted to info",
|
|
700
|
+
file=sys.stderr,
|
|
701
|
+
)
|
|
702
|
+
if args.type == "verification" and (not args.name or not args.status):
|
|
703
|
+
print(
|
|
704
|
+
"warning: verification missing --name or --status",
|
|
705
|
+
file=sys.stderr,
|
|
706
|
+
)
|
|
630
707
|
line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
631
708
|
try:
|
|
632
709
|
with event_file_lock(lock_path):
|
|
@@ -149,7 +149,8 @@ def checkpoint_status(checkpoints: dict[str, Any] | None, checkpoint_id: str) ->
|
|
|
149
149
|
def foundation_gate_blocks(task_number: int | None, change_dir: Path) -> dict[str, Any] | None:
|
|
150
150
|
checkpoints = load_checkpoints(change_dir)
|
|
151
151
|
status = checkpoint_status(checkpoints, "foundation-gate")
|
|
152
|
-
|
|
152
|
+
# missing = checkpoint not enabled for this change (no file / no entry).
|
|
153
|
+
if status in {"approved", "missing"}:
|
|
153
154
|
return None
|
|
154
155
|
if task_number is None:
|
|
155
156
|
return {
|
|
@@ -174,8 +175,25 @@ def foundation_gate_blocks(task_number: int | None, change_dir: Path) -> dict[st
|
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
|
|
177
|
-
def
|
|
178
|
+
def is_degraded_ledger_entry(entry: dict[str, Any]) -> bool:
|
|
179
|
+
"""NOT_RUN + evidence starting with 'DEGRADED: <non-empty reason>'."""
|
|
180
|
+
if entry.get("status") != "NOT_RUN":
|
|
181
|
+
return False
|
|
182
|
+
evidence = str(entry.get("evidence") or "").strip()
|
|
183
|
+
if not evidence.startswith("DEGRADED:"):
|
|
184
|
+
return False
|
|
185
|
+
reason = evidence.split(":", 1)[1].strip()
|
|
186
|
+
return bool(reason)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> tuple[list[str], bool]:
|
|
190
|
+
"""Internal helper for ledger close validation (not a public API).
|
|
191
|
+
|
|
192
|
+
Returns ``(missing_fields, degraded_ok)`` where ``degraded_ok`` is True only when
|
|
193
|
+
the entry is a valid DEGRADED NOT_RUN record with no other missing fields.
|
|
194
|
+
"""
|
|
178
195
|
missing: list[str] = []
|
|
196
|
+
degraded = is_degraded_ledger_entry(entry)
|
|
179
197
|
for field in LEDGER_V2_REQUIRED_ENTRY_FIELDS:
|
|
180
198
|
value = entry.get(field)
|
|
181
199
|
if field == "inputsFiles":
|
|
@@ -184,7 +202,7 @@ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[s
|
|
|
184
202
|
elif verification == "unitTestFull" and not value:
|
|
185
203
|
missing.append("inputsFiles(non-empty)")
|
|
186
204
|
elif field == "status":
|
|
187
|
-
if value != "OK":
|
|
205
|
+
if value != "OK" and not degraded:
|
|
188
206
|
missing.append("status=OK")
|
|
189
207
|
elif not (isinstance(value, str) and value.strip()):
|
|
190
208
|
missing.append(field)
|
|
@@ -192,7 +210,7 @@ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[s
|
|
|
192
210
|
missing.append("coverage(valid)")
|
|
193
211
|
elif field == "algorithmVersion" and str(value).strip() != hl.LEDGER_VERSION:
|
|
194
212
|
missing.append("algorithmVersion(harness-ledger-2)")
|
|
195
|
-
return missing
|
|
213
|
+
return missing, degraded and not missing
|
|
196
214
|
|
|
197
215
|
|
|
198
216
|
def validate_ledger_for_phase_close(
|
|
@@ -225,6 +243,7 @@ def validate_ledger_for_phase_close(
|
|
|
225
243
|
}
|
|
226
244
|
|
|
227
245
|
problems: list[dict[str, Any]] = []
|
|
246
|
+
degraded: list[str] = []
|
|
228
247
|
for verification in required:
|
|
229
248
|
entry = validations.get(verification)
|
|
230
249
|
if not isinstance(entry, dict):
|
|
@@ -236,7 +255,7 @@ def validate_ledger_for_phase_close(
|
|
|
236
255
|
}
|
|
237
256
|
)
|
|
238
257
|
continue
|
|
239
|
-
missing = validate_ledger_entry_v2(entry, verification)
|
|
258
|
+
missing, is_degraded = validate_ledger_entry_v2(entry, verification)
|
|
240
259
|
if missing:
|
|
241
260
|
code = (
|
|
242
261
|
"MISSING_V2_FIELDS"
|
|
@@ -258,6 +277,8 @@ def validate_ledger_for_phase_close(
|
|
|
258
277
|
"code": code,
|
|
259
278
|
}
|
|
260
279
|
)
|
|
280
|
+
elif is_degraded:
|
|
281
|
+
degraded.append(verification)
|
|
261
282
|
|
|
262
283
|
if problems:
|
|
263
284
|
return {
|
|
@@ -269,6 +290,15 @@ def validate_ledger_for_phase_close(
|
|
|
269
290
|
"ledgerPath": str(ledger_path) if ledger_path else None,
|
|
270
291
|
"detail": "natural-language override is not permitted",
|
|
271
292
|
}
|
|
293
|
+
if degraded:
|
|
294
|
+
return {
|
|
295
|
+
"ok": True,
|
|
296
|
+
"code": "LEDGER_OK_DEGRADED",
|
|
297
|
+
"phase": phase,
|
|
298
|
+
"validated": required,
|
|
299
|
+
"degraded": degraded,
|
|
300
|
+
"ledgerPath": str(ledger_path) if ledger_path else None,
|
|
301
|
+
}
|
|
272
302
|
return {
|
|
273
303
|
"ok": True,
|
|
274
304
|
"code": "LEDGER_OK",
|
|
@@ -417,7 +447,11 @@ def change_code_root(change_dir: Path) -> Path:
|
|
|
417
447
|
return project
|
|
418
448
|
|
|
419
449
|
|
|
420
|
-
def classify_risk(
|
|
450
|
+
def classify_risk(
|
|
451
|
+
change_dir: Path,
|
|
452
|
+
stage: str,
|
|
453
|
+
workflow: dict[str, Any] | None = None,
|
|
454
|
+
) -> dict[str, Any]:
|
|
421
455
|
tier = "full"
|
|
422
456
|
source = "default-full"
|
|
423
457
|
plan_path = change_dir / "plans"
|
|
@@ -484,7 +518,31 @@ def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
|
|
|
484
518
|
tier = observed
|
|
485
519
|
source = f"{source}+post-run"
|
|
486
520
|
main_project = change_dir.parents[2]
|
|
487
|
-
workflow
|
|
521
|
+
if workflow is None:
|
|
522
|
+
workflow = _load_workflow_policy(project=main_project)
|
|
523
|
+
tier_policy = workflow["riskTiers"][tier]
|
|
524
|
+
unique_signals = sorted(set(signals))
|
|
525
|
+
payload = {
|
|
526
|
+
"ok": True,
|
|
527
|
+
"code": "CLASSIFIED",
|
|
528
|
+
"stage": stage,
|
|
529
|
+
"tier": tier,
|
|
530
|
+
"source": source,
|
|
531
|
+
"changeId": change_dir.name,
|
|
532
|
+
"signals": unique_signals,
|
|
533
|
+
"defaultPhases": list(tier_policy["defaultPhases"]),
|
|
534
|
+
"requiredValidations": list(tier_policy["requiredValidations"]),
|
|
535
|
+
"conditionalStages": list(tier_policy["conditionalStages"]),
|
|
536
|
+
"stageDecisions": _stage_decisions_for_tier(workflow, tier, unique_signals),
|
|
537
|
+
}
|
|
538
|
+
if stage == "post-run":
|
|
539
|
+
_write_json(change_dir / "meta" / "risk-classification.json", payload)
|
|
540
|
+
return payload
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _stage_decisions_for_tier(
|
|
544
|
+
workflow: dict[str, Any], tier: str, signals: list[str]
|
|
545
|
+
) -> dict[str, dict[str, Any]]:
|
|
488
546
|
tier_policy = workflow["riskTiers"][tier]
|
|
489
547
|
unique_signals = sorted(set(signals))
|
|
490
548
|
stage_decisions: dict[str, dict[str, Any]] = {}
|
|
@@ -501,22 +559,66 @@ def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
|
|
|
501
559
|
),
|
|
502
560
|
"matchedSignals": matched,
|
|
503
561
|
}
|
|
504
|
-
|
|
562
|
+
return stage_decisions
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def apply_tier_override(
|
|
566
|
+
payload: dict[str, Any],
|
|
567
|
+
workflow: dict[str, Any],
|
|
568
|
+
*,
|
|
569
|
+
tier: str,
|
|
570
|
+
override_by: str,
|
|
571
|
+
) -> dict[str, Any]:
|
|
572
|
+
"""Rebind payload to an explicit tier override (source=override)."""
|
|
573
|
+
tier_policy = workflow["riskTiers"][tier]
|
|
574
|
+
now = hc.now_iso()
|
|
575
|
+
signals = list(payload.get("signals") or [])
|
|
576
|
+
payload = dict(payload)
|
|
577
|
+
payload["tier"] = tier
|
|
578
|
+
payload["source"] = "override"
|
|
579
|
+
payload["defaultPhases"] = list(tier_policy["defaultPhases"])
|
|
580
|
+
payload["requiredValidations"] = list(tier_policy["requiredValidations"])
|
|
581
|
+
payload["conditionalStages"] = list(tier_policy["conditionalStages"])
|
|
582
|
+
payload["stageDecisions"] = _stage_decisions_for_tier(workflow, tier, signals)
|
|
583
|
+
payload["tierOverride"] = {"tier": tier, "by": override_by, "at": now}
|
|
584
|
+
return payload
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def gate_policy_document(payload: dict[str, Any]) -> dict[str, Any]:
|
|
588
|
+
"""Cross-change contract: meta/gate-policy.json (schemaVersion=1)."""
|
|
589
|
+
return {
|
|
590
|
+
"schemaVersion": 1,
|
|
591
|
+
"tier": payload["tier"],
|
|
592
|
+
"source": payload["source"],
|
|
593
|
+
"defaultPhases": list(payload.get("defaultPhases") or []),
|
|
594
|
+
"requiredValidations": list(payload.get("requiredValidations") or []),
|
|
595
|
+
"classifiedAt": payload.get("classifiedAt") or hc.now_iso(),
|
|
596
|
+
"tierOverride": payload.get("tierOverride"),
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def classify_defaults(
|
|
601
|
+
workflow: dict[str, Any],
|
|
602
|
+
*,
|
|
603
|
+
change_id: str,
|
|
604
|
+
stage: str,
|
|
605
|
+
) -> dict[str, Any]:
|
|
606
|
+
tier = "full"
|
|
607
|
+
source = "default-full"
|
|
608
|
+
tier_policy = workflow["riskTiers"][tier]
|
|
609
|
+
return {
|
|
505
610
|
"ok": True,
|
|
506
611
|
"code": "CLASSIFIED",
|
|
507
612
|
"stage": stage,
|
|
508
613
|
"tier": tier,
|
|
509
614
|
"source": source,
|
|
510
|
-
"changeId":
|
|
511
|
-
"signals":
|
|
615
|
+
"changeId": change_id,
|
|
616
|
+
"signals": [],
|
|
512
617
|
"defaultPhases": list(tier_policy["defaultPhases"]),
|
|
513
618
|
"requiredValidations": list(tier_policy["requiredValidations"]),
|
|
514
619
|
"conditionalStages": list(tier_policy["conditionalStages"]),
|
|
515
|
-
"stageDecisions":
|
|
620
|
+
"stageDecisions": _stage_decisions_for_tier(workflow, tier, []),
|
|
516
621
|
}
|
|
517
|
-
if stage == "post-run":
|
|
518
|
-
_write_json(change_dir / "meta" / "risk-classification.json", payload)
|
|
519
|
-
return payload
|
|
520
622
|
|
|
521
623
|
|
|
522
624
|
def lint_skill_tree(skills_root: Path) -> dict[str, Any]:
|
|
@@ -823,6 +925,14 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
823
925
|
extra={k: v for k, v in ledger_result.items() if k not in {"ok", "message", "code"}},
|
|
824
926
|
)
|
|
825
927
|
|
|
928
|
+
close_status = args.status
|
|
929
|
+
close_code = "PHASE_CLOSED"
|
|
930
|
+
if ledger_result.get("code") == "LEDGER_OK_DEGRADED":
|
|
931
|
+
close_code = "CLOSED_DEGRADED"
|
|
932
|
+
# Degraded close: phase.end status must not exceed WARN (OK → WARN).
|
|
933
|
+
if close_status == "OK":
|
|
934
|
+
close_status = "WARN"
|
|
935
|
+
|
|
826
936
|
guard_result = None
|
|
827
937
|
if args.phase in {"run", "test"}:
|
|
828
938
|
project_root = args.project or str(project)
|
|
@@ -839,7 +949,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
839
949
|
change_dir,
|
|
840
950
|
phase=args.phase,
|
|
841
951
|
type_="phase.end",
|
|
842
|
-
status=
|
|
952
|
+
status=close_status,
|
|
843
953
|
note=args.note or "",
|
|
844
954
|
run_id=run_id,
|
|
845
955
|
) if not _phase_event_exists(change_dir, args.phase, "phase.end", run_id) else {
|
|
@@ -862,9 +972,9 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
862
972
|
|
|
863
973
|
payload = {
|
|
864
974
|
"ok": True,
|
|
865
|
-
"code":
|
|
975
|
+
"code": close_code,
|
|
866
976
|
"phase": args.phase,
|
|
867
|
-
"status":
|
|
977
|
+
"status": close_status,
|
|
868
978
|
"changeId": resolved["changeId"],
|
|
869
979
|
"ledger": ledger_result,
|
|
870
980
|
"testGuard": guard_result,
|
|
@@ -878,19 +988,56 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
878
988
|
def cmd_classify(args: argparse.Namespace) -> int:
|
|
879
989
|
as_json = bool(args.json)
|
|
880
990
|
project = hc.resolve_main_project_root()
|
|
991
|
+
try:
|
|
992
|
+
workflow = _load_workflow_policy(project=project)
|
|
993
|
+
except (OSError, ValueError, hwp.PolicyValidationError) as exc:
|
|
994
|
+
return emit_error("POLICY_LOAD_FAILED", str(exc), as_json=as_json)
|
|
995
|
+
|
|
881
996
|
resolved = hc.resolve_change(project, args.change)
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
997
|
+
change_dir: Path | None = None
|
|
998
|
+
change_id = str(args.change or "")
|
|
999
|
+
if resolved.get("ok"):
|
|
1000
|
+
change_dir = Path(resolved["changeDir"])
|
|
1001
|
+
change_id = str(resolved.get("changeId") or change_dir.name)
|
|
1002
|
+
if not change_dir.is_dir():
|
|
1003
|
+
change_dir = None
|
|
1004
|
+
|
|
1005
|
+
if change_dir is not None:
|
|
1006
|
+
payload = classify_risk(change_dir, args.stage, workflow=workflow)
|
|
1007
|
+
else:
|
|
1008
|
+
payload = classify_defaults(workflow, change_id=change_id or "unknown", stage=args.stage)
|
|
1009
|
+
if not resolved.get("ok"):
|
|
1010
|
+
payload["resolveCode"] = resolved.get("code")
|
|
1011
|
+
|
|
1012
|
+
tier_override = getattr(args, "tier_override", None)
|
|
1013
|
+
if tier_override:
|
|
1014
|
+
payload = apply_tier_override(
|
|
1015
|
+
payload,
|
|
1016
|
+
workflow,
|
|
1017
|
+
tier=str(tier_override),
|
|
1018
|
+
override_by=str(getattr(args, "override_by", None) or "user"),
|
|
888
1019
|
)
|
|
889
|
-
|
|
1020
|
+
else:
|
|
1021
|
+
payload.setdefault("tierOverride", None)
|
|
1022
|
+
|
|
1023
|
+
classified_at = hc.now_iso()
|
|
1024
|
+
payload["classifiedAt"] = classified_at
|
|
1025
|
+
|
|
1026
|
+
if change_dir is not None and change_dir.is_dir():
|
|
1027
|
+
policy_doc = gate_policy_document(payload)
|
|
1028
|
+
policy_path = change_dir / "meta" / "gate-policy.json"
|
|
1029
|
+
_write_json(policy_path, policy_doc)
|
|
1030
|
+
payload["policyPersisted"] = True
|
|
1031
|
+
payload["policyPath"] = str(policy_path)
|
|
1032
|
+
else:
|
|
1033
|
+
payload["policyPersisted"] = False
|
|
1034
|
+
payload["warning"] = (
|
|
1035
|
+
"change directory does not exist; gate-policy.json not written"
|
|
1036
|
+
)
|
|
1037
|
+
|
|
890
1038
|
emit(payload, as_json=as_json)
|
|
891
1039
|
return 0
|
|
892
1040
|
|
|
893
|
-
|
|
894
1041
|
def cmd_checkpoint(args: argparse.Namespace) -> int:
|
|
895
1042
|
as_json = bool(args.json)
|
|
896
1043
|
project = hc.resolve_main_project_root()
|
|
@@ -1012,6 +1159,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1012
1159
|
p_classify = sub.add_parser("classify", parents=[shared])
|
|
1013
1160
|
p_classify.add_argument("--change", default=None)
|
|
1014
1161
|
p_classify.add_argument("--stage", required=True, choices=["plan", "post-run"])
|
|
1162
|
+
p_classify.add_argument(
|
|
1163
|
+
"--tier-override",
|
|
1164
|
+
default=None,
|
|
1165
|
+
choices=["fast", "standard", "full"],
|
|
1166
|
+
)
|
|
1167
|
+
p_classify.add_argument("--override-by", default="user")
|
|
1015
1168
|
p_classify.set_defaults(func=cmd_classify)
|
|
1016
1169
|
|
|
1017
1170
|
p_checkpoint = sub.add_parser("checkpoint", parents=[shared])
|
|
@@ -816,6 +816,18 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
816
816
|
"finishedAt": now_iso(),
|
|
817
817
|
}
|
|
818
818
|
)
|
|
819
|
+
metrics_raw = getattr(args, "metrics_json", None)
|
|
820
|
+
if metrics_raw is not None and str(metrics_raw).strip() != "":
|
|
821
|
+
try:
|
|
822
|
+
metrics_obj = json.loads(str(metrics_raw))
|
|
823
|
+
except json.JSONDecodeError:
|
|
824
|
+
return emit_error("invalid --metrics-json", as_json=as_json)
|
|
825
|
+
if not isinstance(metrics_obj, dict):
|
|
826
|
+
return emit_error("invalid --metrics-json", as_json=as_json)
|
|
827
|
+
entry["metrics"] = metrics_obj
|
|
828
|
+
elif "metrics" in entry:
|
|
829
|
+
# Fresh record without metrics must not keep stale metrics from prev.
|
|
830
|
+
entry.pop("metrics", None)
|
|
819
831
|
if args.scope is not None and str(args.scope).strip():
|
|
820
832
|
entry["scope"] = str(args.scope).strip()
|
|
821
833
|
# No default scope: recording an incremental run as broad "module" scope
|
|
@@ -1014,6 +1026,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1014
1026
|
default=None,
|
|
1015
1027
|
help="package: prior verifications reused when testsExecuted=false",
|
|
1016
1028
|
)
|
|
1029
|
+
p_record.add_argument(
|
|
1030
|
+
"--metrics-json",
|
|
1031
|
+
default=None,
|
|
1032
|
+
help='structured counts, e.g. \'{"run":155,"failures":0}\' or \'{"total":3,"passed":3}\'',
|
|
1033
|
+
)
|
|
1017
1034
|
p_record.set_defaults(func=cmd_record)
|
|
1018
1035
|
|
|
1019
1036
|
p_diff = sub.add_parser(
|
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"riskTiers": {
|
|
4
4
|
"fast": {
|
|
5
|
-
"description": "Low-risk docs/comments/non-behavioral config; no API/DB/security/concurrency.",
|
|
5
|
+
"description": "Low-risk docs/comments/non-behavioral config; no API/DB/security/concurrency. Policy notes: checkpointRules apply only when meta/implementation-checkpoints.json defines the entry for this change; missing file/entry = disabled (gate must not require --task).",
|
|
6
6
|
"defaultPhases": ["plan", "run", "submit", "archive"],
|
|
7
7
|
"requiredValidations": ["unitTest"],
|
|
8
8
|
"conditionalStages": ["review", "package", "apidoc"],
|
|
9
9
|
"upgradeTriggers": ["api-change", "security", "migration", "concurrency", "breaking-contract"]
|
|
10
10
|
},
|
|
11
11
|
"standard": {
|
|
12
|
-
"description": "Ordinary production code and non-breaking API/features.",
|
|
12
|
+
"description": "Ordinary production code and non-breaking API/features. Policy notes: requiredValidations may close DEGRADED when ledger status=NOT_RUN and evidence starts with 'DEGRADED: <reason>' (close code CLOSED_DEGRADED, phase status <= WARN); do not fake OK with unrelated tests.",
|
|
13
13
|
"defaultPhases": ["plan", "run", "test", "submit", "archive"],
|
|
14
14
|
"requiredValidations": ["compile", "unitTest", "unitTestFull"],
|
|
15
15
|
"conditionalStages": ["review", "package", "apidoc"],
|
|
16
16
|
"upgradeTriggers": ["auth", "security", "delete", "migration", "concurrency", "artifact-protocol", "shared-state", "breaking-api"]
|
|
17
17
|
},
|
|
18
18
|
"full": {
|
|
19
|
-
"description": "Auth/security/delete/migration/concurrency/service protocol/shared state/breaking contracts.",
|
|
19
|
+
"description": "Auth/security/delete/migration/concurrency/service protocol/shared state/breaking contracts. Policy notes: (1) DEGRADED close — ledger status=NOT_RUN and evidence 'DEGRADED: <reason>' → CLOSED_DEGRADED with phase <= WARN; never fake OK with unrelated tests. (2) checkpointRules — only when plan created meta/implementation-checkpoints.json entry; missing file/entry = not enabled, gate must not require --task.",
|
|
20
20
|
"defaultPhases": ["plan", "run", "test", "review", "submit", "archive"],
|
|
21
21
|
"requiredValidations": ["compile", "unitTest", "unitTestFull", "apiTest"],
|
|
22
22
|
"conditionalStages": ["package", "apidoc"],
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=8d70b36389868486; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=8d70b36389868486; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-archive — 变更归档
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -103,8 +103,8 @@ AskUserQuestion 让用户确认归档操作。**用户拒绝 → 终止流程,
|
|
|
103
103
|
### Phase 3:执行归档
|
|
104
104
|
|
|
105
105
|
1. 运行 `python <skills-root>/scripts/harness_archive.py status --change-dir ".harness/changes/<change-name>" --json` 前置检查。
|
|
106
|
-
2.
|
|
107
|
-
3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --json`;读 JSON
|
|
106
|
+
2. `meta/archive-meta.md` **由 `harness_archive.py finalize` 自动生成**(与 summary-data `finalStatus` 同源);**禁止 agent 手写**该文件,手写视为数据丢失。维护者结论写入 events(decision/issue)即可,finalize 会汇总到 summary / archive-meta。
|
|
107
|
+
3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --json`;读 JSON(cleanup、事件、移动、collect、render、validate、manifest 比对、archive-meta)。finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加。**finalize 不再同步执行知识维护**(§8.2):它写一个 `pending` maintenance-outbox 项即返回,`knowledgeMaintenance=QUEUED`;写 outbox 失败时 `NOT_QUEUED`(warning,不回滚 archive,总状态 CONDITIONAL)。后续由 `harness-sync` / `harness_knowledge.py maintain` 异步推进 outbox。**finalize 失败或 validate 报错时不删除原目录**。
|
|
108
108
|
|
|
109
109
|
- **Read `reference.md`** — finalize 输出字段、archive-meta 格式、CONDITIONAL_OK 规则
|
|
110
110
|
- **Read `templates/summary-data-template.json`** — summary-data 数据结构
|