@blxzer/cursor-trellis 0.4.2 → 0.4.3
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 +24 -0
- package/README.md +23 -14
- package/README.zh-CN.md +14 -10
- package/bin/smart-search.js +10 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +36 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +42 -153
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts +10 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +38 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/update.d.ts +5 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +137 -40
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/cursor.d.ts +2 -1
- package/dist/configurators/cursor.d.ts.map +1 -1
- package/dist/configurators/cursor.js +4 -2
- package/dist/configurators/cursor.js.map +1 -1
- package/dist/configurators/index.js +4 -4
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/shared.d.ts +8 -0
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +27 -1
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts +0 -9
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +2 -4
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +9 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +13 -0
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.3.0.json +1 -1
- package/dist/templates/common/bundled-skills/cstl-meta/references/platform-files/skills-and-commands.md +2 -4
- package/dist/templates/common/bundled-skills/cstl-skill-creator/SKILL.md +10 -7
- package/dist/templates/common/bundled-skills/cstl-skill-creator/references/authoring-rules.md +93 -19
- package/dist/templates/common/bundled-skills/cstl-skill-creator/references/general-authoring.md +189 -0
- package/dist/templates/common/bundled-skills/cstl-skill-creator/references/review-checklist.md +10 -5
- package/dist/templates/common/commands/continue.md +5 -4
- package/dist/templates/common/index.d.ts +20 -0
- package/dist/templates/common/index.d.ts.map +1 -1
- package/dist/templates/common/index.js +21 -2
- package/dist/templates/common/index.js.map +1 -1
- package/dist/templates/common/optional-skills/chrome-cdp/SKILL.md +179 -0
- package/dist/templates/common/optional-skills/chrome-cdp/examples/fetch-hook-api-capture.md +149 -0
- package/dist/templates/common/optional-skills/chrome-cdp/scripts/cdp.mjs +903 -0
- package/dist/templates/cursor/agents/cstl-research.md +1 -1
- 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 +7 -24
- package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
- package/dist/templates/cursor/hooks.json +4 -0
- package/dist/templates/cursor/index.d.ts +1 -1
- package/dist/templates/cursor/index.js +1 -1
- package/dist/templates/cursor/rules/cstl-bootstrap.mdc +17 -0
- package/dist/templates/markdown/agents.md +15 -10
- package/dist/templates/markdown/framework/cursor-native-modes-guide.md.txt +12 -9
- package/dist/templates/markdown/framework/cursor-subagent-policy.md.txt +62 -126
- package/dist/templates/markdown/framework/dogfood-only-surfaces.md.txt +4 -6
- package/dist/templates/markdown/framework/execution-strategy.md.txt +2 -0
- package/dist/templates/markdown/framework/index.md.txt +4 -0
- package/dist/templates/markdown/framework/internal-skills-cursor-reachability.md.txt +4 -3
- package/dist/templates/markdown/framework/middleware-protocol.md.txt +83 -0
- package/dist/templates/markdown/framework/parallel-first-execution.md.txt +78 -0
- package/dist/templates/markdown/framework/release-boundary.md.txt +30 -0
- package/dist/templates/markdown/framework/retrieval-daily-guide.md.txt +1 -1
- package/dist/templates/markdown/framework/upgrade.md.txt +15 -0
- package/dist/templates/markdown/index.d.ts +4 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +17 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/index.md.txt +0 -6
- package/dist/templates/shared-hooks/event-bridge.py +60 -0
- package/dist/templates/shared-hooks/index.d.ts +1 -1
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +1 -0
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/session-start.py +18 -1
- package/dist/templates/trellis/CONTEXT.md +63 -3
- package/dist/templates/trellis/gitignore.txt +39 -39
- package/dist/templates/trellis/index.d.ts +12 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +73 -15
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/pool/README.md +12 -3
- package/dist/templates/trellis/pool/plan.md +24 -5
- package/dist/templates/trellis/scripts/common/adapter_middleware.py +158 -0
- package/dist/templates/trellis/scripts/common/artifact_locale.py +12 -3
- package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +5 -2
- package/dist/templates/trellis/scripts/common/full_quality.py +237 -0
- package/dist/templates/trellis/scripts/common/git.py +6 -0
- package/dist/templates/trellis/scripts/common/git_context.py +17 -5
- package/dist/templates/trellis/scripts/common/kernel_command.py +367 -0
- package/dist/templates/trellis/scripts/common/lite_context.py +280 -0
- package/dist/templates/trellis/scripts/common/ondemand_topology.py +225 -0
- package/dist/templates/trellis/scripts/common/parent_orchestration.py +1 -1
- package/dist/templates/trellis/scripts/common/pool_store.py +16 -4
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +2 -2
- package/dist/templates/trellis/scripts/common/session_context.py +28 -2
- package/dist/templates/trellis/scripts/common/task_gates.py +73 -12
- package/dist/templates/trellis/scripts/common/task_map.py +42 -9
- package/dist/templates/trellis/scripts/common/task_store.py +286 -56
- package/dist/templates/trellis/scripts/common/test_adapter_middleware.py +56 -0
- package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +9 -0
- package/dist/templates/trellis/scripts/common/test_full_quality.py +197 -0
- package/dist/templates/trellis/scripts/common/test_kernel_command.py +326 -0
- package/dist/templates/trellis/scripts/common/test_lite_path.py +190 -0
- package/dist/templates/trellis/scripts/common/test_ondemand_topology.py +122 -0
- package/dist/templates/trellis/scripts/common/test_pool_store.py +17 -0
- package/dist/templates/trellis/scripts/common/test_task_store_kernel_patch.py +163 -0
- package/dist/templates/trellis/scripts/common/workflow_phase.py +3 -0
- package/dist/templates/trellis/scripts/get_context.py +1 -0
- package/dist/templates/trellis/scripts/pool.py +5 -0
- package/dist/templates/trellis/scripts/task.py +83 -7
- package/dist/templates/trellis/workflow.md +100 -18
- package/dist/utils/cursor-retrieval-env.d.ts +2 -1
- package/dist/utils/cursor-retrieval-env.d.ts.map +1 -1
- package/dist/utils/cursor-retrieval-env.js +3 -2
- package/dist/utils/cursor-retrieval-env.js.map +1 -1
- package/dist/utils/cursor2plus-residue-cleanup.d.ts +38 -0
- package/dist/utils/cursor2plus-residue-cleanup.d.ts.map +1 -0
- package/dist/utils/cursor2plus-residue-cleanup.js +145 -0
- package/dist/utils/cursor2plus-residue-cleanup.js.map +1 -0
- package/dist/utils/manifest-prune.d.ts.map +1 -1
- package/dist/utils/manifest-prune.js +7 -2
- package/dist/utils/manifest-prune.js.map +1 -1
- package/dist/utils/p36-upgrade.d.ts +62 -0
- package/dist/utils/p36-upgrade.d.ts.map +1 -0
- package/dist/utils/p36-upgrade.js +160 -0
- package/dist/utils/p36-upgrade.js.map +1 -0
- package/dist/utils/project-capabilities.d.ts +1 -12
- package/dist/utils/project-capabilities.d.ts.map +1 -1
- package/dist/utils/project-capabilities.js +3 -100
- 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 +58 -26
- 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/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +11 -1
- package/dist/utils/template-hash.js.map +1 -1
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
- package/dist/utils/uninstall-scrubbers.js +52 -14
- package/dist/utils/uninstall-scrubbers.js.map +1 -1
- package/dist/utils/update-rollout-report.d.ts +14 -0
- package/dist/utils/update-rollout-report.d.ts.map +1 -1
- package/dist/utils/update-rollout-report.js +1 -0
- package/dist/utils/update-rollout-report.js.map +1 -1
- package/dist/utils/workflow-ownership.d.ts +1 -1
- package/dist/utils/workflow-ownership.js +4 -4
- package/dist/utils/workflow-ownership.js.map +1 -1
- package/package.json +5 -4
- package/scripts/postinstall.js +6 -7
- package/dist/commands/campaign/canvas-render.d.ts +0 -68
- package/dist/commands/campaign/canvas-render.d.ts.map +0 -1
- package/dist/commands/campaign/canvas-render.js +0 -414
- package/dist/commands/campaign/canvas-render.js.map +0 -1
- package/dist/commands/campaign/compose.d.ts +0 -15
- package/dist/commands/campaign/compose.d.ts.map +0 -1
- package/dist/commands/campaign/compose.js +0 -110
- package/dist/commands/campaign/compose.js.map +0 -1
- package/dist/commands/campaign/index.d.ts +0 -13
- package/dist/commands/campaign/index.d.ts.map +0 -1
- package/dist/commands/campaign/index.js +0 -134
- package/dist/commands/campaign/index.js.map +0 -1
- package/dist/commands/campaign/kind-map.d.ts +0 -6
- package/dist/commands/campaign/kind-map.d.ts.map +0 -1
- package/dist/commands/campaign/kind-map.js +0 -21
- package/dist/commands/campaign/kind-map.js.map +0 -1
- package/dist/commands/campaign/mcp-server.d.ts +0 -6
- package/dist/commands/campaign/mcp-server.d.ts.map +0 -1
- package/dist/commands/campaign/mcp-server.js +0 -167
- package/dist/commands/campaign/mcp-server.js.map +0 -1
- package/dist/commands/campaign/render.d.ts +0 -5
- package/dist/commands/campaign/render.d.ts.map +0 -1
- package/dist/commands/campaign/render.js +0 -103
- package/dist/commands/campaign/render.js.map +0 -1
- package/dist/commands/campaign/trellis-load.d.ts +0 -13
- package/dist/commands/campaign/trellis-load.d.ts.map +0 -1
- package/dist/commands/campaign/trellis-load.js +0 -122
- package/dist/commands/campaign/trellis-load.js.map +0 -1
- package/dist/commands/campaign/types.d.ts +0 -61
- package/dist/commands/campaign/types.d.ts.map +0 -1
- package/dist/commands/campaign/types.js +0 -3
- package/dist/commands/campaign/types.js.map +0 -1
- package/dist/commands/goal/index.d.ts +0 -3
- package/dist/commands/goal/index.d.ts.map +0 -1
- package/dist/commands/goal/index.js +0 -159
- package/dist/commands/goal/index.js.map +0 -1
- package/dist/commands/goal/review.d.ts +0 -6
- package/dist/commands/goal/review.d.ts.map +0 -1
- package/dist/commands/goal/review.js +0 -28
- package/dist/commands/goal/review.js.map +0 -1
- package/dist/commands/rpc/client.d.ts +0 -51
- package/dist/commands/rpc/client.d.ts.map +0 -1
- package/dist/commands/rpc/client.js +0 -147
- package/dist/commands/rpc/client.js.map +0 -1
- package/dist/commands/rpc/index.d.ts +0 -10
- package/dist/commands/rpc/index.d.ts.map +0 -1
- package/dist/commands/rpc/index.js +0 -157
- package/dist/commands/rpc/index.js.map +0 -1
- package/dist/commands/rpc/serve.d.ts +0 -23
- package/dist/commands/rpc/serve.d.ts.map +0 -1
- package/dist/commands/rpc/serve.js +0 -60
- package/dist/commands/rpc/serve.js.map +0 -1
- package/dist/commands/rpc/smoke.d.ts +0 -16
- package/dist/commands/rpc/smoke.d.ts.map +0 -1
- package/dist/commands/rpc/smoke.js +0 -92
- package/dist/commands/rpc/smoke.js.map +0 -1
- package/dist/commands/sdk/index.d.ts +0 -9
- package/dist/commands/sdk/index.d.ts.map +0 -1
- package/dist/commands/sdk/index.js +0 -58
- package/dist/commands/sdk/index.js.map +0 -1
- package/dist/commands/sdk/run.d.ts +0 -42
- package/dist/commands/sdk/run.d.ts.map +0 -1
- package/dist/commands/sdk/run.js +0 -220
- package/dist/commands/sdk/run.js.map +0 -1
- package/dist/commands/sdk/status.d.ts +0 -14
- package/dist/commands/sdk/status.d.ts.map +0 -1
- package/dist/commands/sdk/status.js +0 -69
- package/dist/commands/sdk/status.js.map +0 -1
- package/dist/configurators/cursor2plus-local.d.ts +0 -6
- package/dist/configurators/cursor2plus-local.d.ts.map +0 -1
- package/dist/configurators/cursor2plus-local.js +0 -21
- package/dist/configurators/cursor2plus-local.js.map +0 -1
- package/dist/goal/action-packet.d.ts +0 -89
- package/dist/goal/action-packet.d.ts.map +0 -1
- package/dist/goal/action-packet.js +0 -113
- package/dist/goal/action-packet.js.map +0 -1
- package/dist/goal/audit.d.ts +0 -14
- package/dist/goal/audit.d.ts.map +0 -1
- package/dist/goal/audit.js +0 -12
- package/dist/goal/audit.js.map +0 -1
- package/dist/goal/constants.d.ts +0 -26
- package/dist/goal/constants.d.ts.map +0 -1
- package/dist/goal/constants.js +0 -24
- package/dist/goal/constants.js.map +0 -1
- package/dist/goal/paths.d.ts +0 -8
- package/dist/goal/paths.d.ts.map +0 -1
- package/dist/goal/paths.js +0 -36
- package/dist/goal/paths.js.map +0 -1
- package/dist/goal/preflight.d.ts +0 -21
- package/dist/goal/preflight.d.ts.map +0 -1
- package/dist/goal/preflight.js +0 -97
- package/dist/goal/preflight.js.map +0 -1
- package/dist/goal/review-seam.d.ts +0 -44
- package/dist/goal/review-seam.d.ts.map +0 -1
- package/dist/goal/review-seam.js +0 -153
- package/dist/goal/review-seam.js.map +0 -1
- package/dist/goal/reviewer-rules.d.ts +0 -13
- package/dist/goal/reviewer-rules.d.ts.map +0 -1
- package/dist/goal/reviewer-rules.js +0 -153
- package/dist/goal/reviewer-rules.js.map +0 -1
- package/dist/goal/reviewer.d.ts +0 -9
- package/dist/goal/reviewer.d.ts.map +0 -1
- package/dist/goal/reviewer.js +0 -54
- package/dist/goal/reviewer.js.map +0 -1
- package/dist/goal/runtime.d.ts +0 -32
- package/dist/goal/runtime.d.ts.map +0 -1
- package/dist/goal/runtime.js +0 -242
- package/dist/goal/runtime.js.map +0 -1
- package/dist/goal/sdk-client.d.ts +0 -10
- package/dist/goal/sdk-client.d.ts.map +0 -1
- package/dist/goal/sdk-client.js +0 -34
- package/dist/goal/sdk-client.js.map +0 -1
- package/dist/goal/state.d.ts +0 -40
- package/dist/goal/state.d.ts.map +0 -1
- package/dist/goal/state.js +0 -37
- package/dist/goal/state.js.map +0 -1
- package/dist/goal/task-triggers.d.ts +0 -15
- package/dist/goal/task-triggers.d.ts.map +0 -1
- package/dist/goal/task-triggers.js +0 -23
- package/dist/goal/task-triggers.js.map +0 -1
- package/dist/goal/walls.d.ts +0 -12
- package/dist/goal/walls.d.ts.map +0 -1
- package/dist/goal/walls.js +0 -65
- package/dist/goal/walls.js.map +0 -1
- package/dist/goal/worker-prompt.d.ts +0 -13
- package/dist/goal/worker-prompt.d.ts.map +0 -1
- package/dist/goal/worker-prompt.js +0 -41
- package/dist/goal/worker-prompt.js.map +0 -1
- package/dist/goal/worker.d.ts +0 -42
- package/dist/goal/worker.d.ts.map +0 -1
- package/dist/goal/worker.js +0 -180
- package/dist/goal/worker.js.map +0 -1
- package/dist/templates/common/bundled-skills/cstl-cursor2plus-setup/SKILL.md +0 -63
- package/dist/templates/cursor/commands/cursor2plus-setup.md +0 -82
- package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +0 -32
- package/dist/templates/cursor/rules/cstl-session-rename.mdc +0 -31
- package/dist/templates/cursor/rules/cstl-subagent-dispatch.mdc +0 -34
- package/dist/templates/cursor/rules/cstl-triage.mdc +0 -36
- package/dist/templates/cursor/rules/retrieval-routing.mdc +0 -78
- package/dist/templates/trellis/local/README.md +0 -85
- package/dist/templates/trellis/local/config.local.json.example +0 -6
- package/dist/templates/trellis/local/index.d.ts +0 -8
- package/dist/templates/trellis/local/index.d.ts.map +0 -1
- package/dist/templates/trellis/local/index.js +0 -15
- package/dist/templates/trellis/local/index.js.map +0 -1
- package/dist/templates/trellis/local/patch_wpelc8.py +0 -421
- package/dist/templates/trellis/local/smoke.py +0 -94
- package/dist/templates/trellis/local/subagent-models.json.example +0 -4
- package/dist/templates/trellis/local/trellis-task-models.json.example +0 -10
- package/dist/templates/trellis/local/trellis-task-models.json5.example +0 -45
- package/dist/templates/trellis/local/trellis_task_models_config.py +0 -200
- package/dist/utils/cursor-sdk-gate.d.ts +0 -10
- package/dist/utils/cursor-sdk-gate.d.ts.map +0 -1
- package/dist/utils/cursor-sdk-gate.js +0 -32
- package/dist/utils/cursor-sdk-gate.js.map +0 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Stage 4 Full Quality helpers (controls, AC ledger, graded Independent Check).
|
|
2
|
+
|
|
3
|
+
Python projection of the Kernel Full Quality contract. Kernel remains the
|
|
4
|
+
writer; these helpers validate extras before archive and prefer persisted
|
|
5
|
+
required_controls over inferring Rigor from files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
FULL_QUALITY_SOURCE = "full-quality-contract"
|
|
16
|
+
FULL_QUALITY_SCHEMA_VERSION = 1
|
|
17
|
+
FULL_BASELINE_CONTROLS = (
|
|
18
|
+
"definition",
|
|
19
|
+
"execution-contract",
|
|
20
|
+
"verification-plan",
|
|
21
|
+
"evidence",
|
|
22
|
+
"independent-check",
|
|
23
|
+
)
|
|
24
|
+
DEFAULT_CONTROL_SURFACES = {
|
|
25
|
+
"definition": "prd.md",
|
|
26
|
+
"execution-contract": "implement.md",
|
|
27
|
+
"verification-plan": "implement.md",
|
|
28
|
+
"evidence": "verify.md",
|
|
29
|
+
"design": "design.md",
|
|
30
|
+
}
|
|
31
|
+
DESIGN_RISK_SIGNALS = {
|
|
32
|
+
"architecture",
|
|
33
|
+
"public-contract",
|
|
34
|
+
"schema",
|
|
35
|
+
"security",
|
|
36
|
+
"secret",
|
|
37
|
+
"framework-semantics",
|
|
38
|
+
"runtime-semantics",
|
|
39
|
+
}
|
|
40
|
+
PLACEHOLDER_VALUE_RE = re.compile(
|
|
41
|
+
r"(?i)^(TBD|TODO|待定|待补充|N/?A|NA|NONE|-|\.\.\.)$"
|
|
42
|
+
)
|
|
43
|
+
AC_ITEM_RE = re.compile(r"(?im)^\s*-\s*\[[ xX]\]\s+(.+)$")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FullQualityError(RuntimeError):
|
|
47
|
+
"""Full Quality contract rejected a start/close request."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def quality_fingerprint(payload: str) -> str:
|
|
51
|
+
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
52
|
+
return f"sha256:{digest}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_required_controls(
|
|
56
|
+
*,
|
|
57
|
+
rigor: str = "lite",
|
|
58
|
+
verification_profile: str | None = None,
|
|
59
|
+
risk_signals: list[str] | None = None,
|
|
60
|
+
policy_requires_design: bool = False,
|
|
61
|
+
explicit_controls: list[str] | None = None,
|
|
62
|
+
surfaces: dict[str, str] | None = None,
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
normalized = "full" if rigor == "full" else "lite"
|
|
65
|
+
signals = [str(item).strip().lower() for item in (risk_signals or []) if item]
|
|
66
|
+
design_required = normalized == "full" and (
|
|
67
|
+
policy_requires_design
|
|
68
|
+
or verification_profile == "architecture"
|
|
69
|
+
or any(signal in DESIGN_RISK_SIGNALS for signal in signals)
|
|
70
|
+
)
|
|
71
|
+
if explicit_controls:
|
|
72
|
+
controls = _unique(explicit_controls)
|
|
73
|
+
elif normalized == "full":
|
|
74
|
+
controls = list(FULL_BASELINE_CONTROLS)
|
|
75
|
+
if design_required:
|
|
76
|
+
controls.append("design")
|
|
77
|
+
else:
|
|
78
|
+
controls = ["definition", "evidence"]
|
|
79
|
+
if design_required and "design" not in controls:
|
|
80
|
+
controls.append("design")
|
|
81
|
+
return {
|
|
82
|
+
"schema_version": FULL_QUALITY_SCHEMA_VERSION,
|
|
83
|
+
"source": FULL_QUALITY_SOURCE,
|
|
84
|
+
"rigor": normalized,
|
|
85
|
+
"controls": controls,
|
|
86
|
+
"surfaces": {**DEFAULT_CONTROL_SURFACES, **(surfaces or {})},
|
|
87
|
+
"resolved_from": {
|
|
88
|
+
"verification_profile": verification_profile,
|
|
89
|
+
"risk_signals": signals,
|
|
90
|
+
"policy_requires_design": policy_requires_design,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def rigor_from_required_controls(task_data: dict | None) -> str | None:
|
|
96
|
+
extras = task_data or {}
|
|
97
|
+
bundle = extras.get("required_controls")
|
|
98
|
+
if not isinstance(bundle, dict):
|
|
99
|
+
return None
|
|
100
|
+
rigor = bundle.get("rigor")
|
|
101
|
+
if rigor in ("lite", "full"):
|
|
102
|
+
return rigor
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_acceptance_items(prd_text: str) -> list[dict[str, str]]:
|
|
107
|
+
items: list[dict[str, str]] = []
|
|
108
|
+
index = 0
|
|
109
|
+
for match in AC_ITEM_RE.finditer(prd_text):
|
|
110
|
+
statement = match.group(1).strip()
|
|
111
|
+
if not statement or PLACEHOLDER_VALUE_RE.match(statement):
|
|
112
|
+
continue
|
|
113
|
+
index += 1
|
|
114
|
+
items.append({"id": f"AC-{index}", "statement": statement})
|
|
115
|
+
return items
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def evaluate_independent_check(
|
|
119
|
+
*,
|
|
120
|
+
mode: str,
|
|
121
|
+
independent_worker_available: bool,
|
|
122
|
+
evidence: str,
|
|
123
|
+
code_fingerprint: str,
|
|
124
|
+
attempted_writes: bool = False,
|
|
125
|
+
) -> dict[str, Any]:
|
|
126
|
+
if attempted_writes:
|
|
127
|
+
raise FullQualityError("Independent Check is read-only; repairs return to Execute")
|
|
128
|
+
if mode not in ("self-review", "true-independent"):
|
|
129
|
+
raise FullQualityError("Independent Check mode must be self-review or true-independent")
|
|
130
|
+
if mode == "true-independent" and not independent_worker_available:
|
|
131
|
+
return {
|
|
132
|
+
"schema_version": FULL_QUALITY_SCHEMA_VERSION,
|
|
133
|
+
"mode": "true-independent",
|
|
134
|
+
"readonly": True,
|
|
135
|
+
"result": "BLOCKED",
|
|
136
|
+
"evidence": evidence.strip(),
|
|
137
|
+
"independent_worker": False,
|
|
138
|
+
"code_fingerprint": code_fingerprint,
|
|
139
|
+
}
|
|
140
|
+
if not evidence.strip() or PLACEHOLDER_VALUE_RE.match(evidence.strip()):
|
|
141
|
+
raise FullQualityError(
|
|
142
|
+
"Independent Check PASS/FAIL requires non-empty evidence (no fake-green)"
|
|
143
|
+
)
|
|
144
|
+
return {
|
|
145
|
+
"schema_version": FULL_QUALITY_SCHEMA_VERSION,
|
|
146
|
+
"mode": mode,
|
|
147
|
+
"readonly": True,
|
|
148
|
+
"result": "PASS",
|
|
149
|
+
"evidence": evidence.strip(),
|
|
150
|
+
"independent_worker": independent_worker_available,
|
|
151
|
+
"code_fingerprint": code_fingerprint,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def full_quality_archive_errors(task_dir: Path, task_data: dict | None) -> list[str]:
|
|
156
|
+
"""Return archive errors for persisted Full required_controls only.
|
|
157
|
+
|
|
158
|
+
Lite and legacy file-heuristic Full are unchanged: this does not force
|
|
159
|
+
Independent Check onto Lite.
|
|
160
|
+
"""
|
|
161
|
+
data = task_data or {}
|
|
162
|
+
bundle = data.get("required_controls")
|
|
163
|
+
if not isinstance(bundle, dict) or bundle.get("rigor") != "full":
|
|
164
|
+
return []
|
|
165
|
+
errors: list[str] = []
|
|
166
|
+
controls = bundle.get("controls")
|
|
167
|
+
if not isinstance(controls, list):
|
|
168
|
+
controls = list(FULL_BASELINE_CONTROLS)
|
|
169
|
+
surfaces = bundle.get("surfaces") if isinstance(bundle.get("surfaces"), dict) else {}
|
|
170
|
+
seen: set[str] = set()
|
|
171
|
+
for control in controls:
|
|
172
|
+
if control in ("independent-check",):
|
|
173
|
+
continue
|
|
174
|
+
relative = surfaces.get(control) or DEFAULT_CONTROL_SURFACES.get(control)
|
|
175
|
+
if not relative or relative in seen:
|
|
176
|
+
continue
|
|
177
|
+
seen.add(relative)
|
|
178
|
+
path = task_dir / relative
|
|
179
|
+
if not path.is_file() or not path.read_text(encoding="utf-8").strip():
|
|
180
|
+
errors.append(f"Full required control {control} missing surface {relative}")
|
|
181
|
+
|
|
182
|
+
ledger = data.get("ac_evidence_ledger")
|
|
183
|
+
if not isinstance(ledger, dict):
|
|
184
|
+
errors.append("Full Close requires a machine-readable AC → Evidence ledger")
|
|
185
|
+
else:
|
|
186
|
+
definition = surfaces.get("definition") or DEFAULT_CONTROL_SURFACES["definition"]
|
|
187
|
+
prd_path = task_dir / definition
|
|
188
|
+
prd_text = prd_path.read_text(encoding="utf-8") if prd_path.is_file() else ""
|
|
189
|
+
acceptance = parse_acceptance_items(prd_text)
|
|
190
|
+
mapped = {
|
|
191
|
+
item.get("ac_id"): item
|
|
192
|
+
for item in ledger.get("items", [])
|
|
193
|
+
if isinstance(item, dict)
|
|
194
|
+
}
|
|
195
|
+
if not acceptance:
|
|
196
|
+
errors.append(f"{definition} has no Acceptance Criteria items to map")
|
|
197
|
+
for item in acceptance:
|
|
198
|
+
mapping = mapped.get(item["id"])
|
|
199
|
+
ref = mapping.get("evidence_ref") if isinstance(mapping, dict) else ""
|
|
200
|
+
if not isinstance(ref, str) or not ref.strip() or PLACEHOLDER_VALUE_RE.match(ref.strip()):
|
|
201
|
+
errors.append(f"AC → Evidence ledger missing mapping for {item['id']}")
|
|
202
|
+
current_source = quality_fingerprint(prd_text)
|
|
203
|
+
if ledger.get("source_fingerprint") and ledger.get("source_fingerprint") != current_source:
|
|
204
|
+
errors.append("AC → Evidence ledger is stale after Definition/code change")
|
|
205
|
+
|
|
206
|
+
check = data.get("independent_check")
|
|
207
|
+
if not isinstance(check, dict):
|
|
208
|
+
errors.append("Full Close requires a graded Independent Check verdict")
|
|
209
|
+
else:
|
|
210
|
+
if check.get("readonly") is False:
|
|
211
|
+
errors.append("Independent Check is read-only; repairs return to Execute")
|
|
212
|
+
mode = check.get("mode") or check.get("assurance")
|
|
213
|
+
if mode == "true-independent" and check.get("independent_worker") is not True:
|
|
214
|
+
errors.append("true-independent Check cannot be satisfied without an independent worker")
|
|
215
|
+
if check.get("result") != "PASS":
|
|
216
|
+
errors.append("Full Close requires Independent Check result PASS")
|
|
217
|
+
evidence = check.get("evidence")
|
|
218
|
+
if not isinstance(evidence, str) or not evidence.strip():
|
|
219
|
+
errors.append("Independent Check PASS requires non-empty evidence (no fake-green)")
|
|
220
|
+
ledger = data.get("ac_evidence_ledger")
|
|
221
|
+
if (
|
|
222
|
+
isinstance(ledger, dict)
|
|
223
|
+
and isinstance(ledger.get("tested_code_fingerprint"), str)
|
|
224
|
+
and isinstance(check.get("code_fingerprint"), str)
|
|
225
|
+
and check.get("code_fingerprint") != ledger.get("tested_code_fingerprint")
|
|
226
|
+
):
|
|
227
|
+
errors.append("Independent Check verdict is stale after code change")
|
|
228
|
+
return errors
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _unique(values: list[str]) -> list[str]:
|
|
232
|
+
out: list[str] = []
|
|
233
|
+
for value in values:
|
|
234
|
+
item = value.strip()
|
|
235
|
+
if item and item not in out:
|
|
236
|
+
out.append(item)
|
|
237
|
+
return out
|
|
@@ -29,3 +29,9 @@ def run_git(args: list[str], cwd: Path | None = None) -> tuple[int, str, str]:
|
|
|
29
29
|
return result.returncode, result.stdout, result.stderr
|
|
30
30
|
except Exception as e:
|
|
31
31
|
return 1, "", str(e)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_git_worktree(path: Path) -> bool:
|
|
35
|
+
"""Return True when path is inside a Git worktree. VCS is On-demand."""
|
|
36
|
+
rc, out, _ = run_git(["rev-parse", "--is-inside-work-tree"], cwd=path)
|
|
37
|
+
return rc == 0 and out.strip().lower() == "true"
|
|
@@ -28,8 +28,7 @@ from .packages_context import (
|
|
|
28
28
|
get_context_packages_json,
|
|
29
29
|
)
|
|
30
30
|
from .paths import get_repo_root
|
|
31
|
-
from .
|
|
32
|
-
from .retrieval_pack_context import output_retrieval_pack_json, read_evidence_input
|
|
31
|
+
from .lite_context import LiteContextPackError, get_lite_context_json
|
|
33
32
|
from .trellis_config import read_trellis_config
|
|
34
33
|
from .workflow_phase import (
|
|
35
34
|
filter_platform,
|
|
@@ -60,13 +59,14 @@ def main() -> None:
|
|
|
60
59
|
parser.add_argument(
|
|
61
60
|
"--mode",
|
|
62
61
|
"-m",
|
|
63
|
-
choices=["default", "record", "packages", "phase", "retrieval-pack"],
|
|
62
|
+
choices=["default", "record", "packages", "phase", "retrieval-pack", "lite"],
|
|
64
63
|
default="default",
|
|
65
64
|
help=(
|
|
66
65
|
"Output mode: default (full context), record (for record-session), "
|
|
67
66
|
"packages (package info only), phase (workflow step extraction), "
|
|
68
67
|
"retrieval-pack (score and pack already-collected retrieval evidence; "
|
|
69
|
-
"use after research collection, not on every SessionStart)"
|
|
68
|
+
"use after research collection, not on every SessionStart), "
|
|
69
|
+
"lite (Personal Lite phase pack with budget; no Parent/VCS/retrieval-extended)"
|
|
70
70
|
),
|
|
71
71
|
)
|
|
72
72
|
parser.add_argument(
|
|
@@ -138,12 +138,18 @@ def main() -> None:
|
|
|
138
138
|
print(content, end="")
|
|
139
139
|
elif args.mode == "retrieval-pack":
|
|
140
140
|
try:
|
|
141
|
+
from .project_file_stats import resolve_project_file_count_arg
|
|
142
|
+
from .retrieval_pack_context import (
|
|
143
|
+
output_retrieval_pack_json,
|
|
144
|
+
read_evidence_input,
|
|
145
|
+
)
|
|
146
|
+
|
|
141
147
|
evidence = read_evidence_input(args.input)
|
|
142
148
|
project_file_count = resolve_project_file_count_arg(
|
|
143
149
|
args.project_file_count,
|
|
144
150
|
repo_root=get_repo_root(),
|
|
145
151
|
)
|
|
146
|
-
except (OSError, json.JSONDecodeError, ValueError) as error:
|
|
152
|
+
except (OSError, json.JSONDecodeError, ValueError, ImportError) as error:
|
|
147
153
|
parser.exit(1, f"retrieval pack error: {error}\n")
|
|
148
154
|
output_retrieval_pack_json(
|
|
149
155
|
evidence_input=evidence,
|
|
@@ -153,6 +159,12 @@ def main() -> None:
|
|
|
153
159
|
pretty=args.json,
|
|
154
160
|
project_file_count=project_file_count,
|
|
155
161
|
)
|
|
162
|
+
elif args.mode == "lite":
|
|
163
|
+
try:
|
|
164
|
+
pack = get_lite_context_json()
|
|
165
|
+
except LiteContextPackError as error:
|
|
166
|
+
parser.exit(1, f"lite context pack error: {error}\n")
|
|
167
|
+
print(json.dumps(pack, indent=2 if args.json else None, ensure_ascii=False))
|
|
156
168
|
else:
|
|
157
169
|
if args.json:
|
|
158
170
|
output_json()
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""JSON CLI bridge to the TypeScript Kernel (P30 Stage 2 strangler).
|
|
2
|
+
|
|
3
|
+
Python create / start-execution / record-gate / archive / patch submit a
|
|
4
|
+
Kernel Command. `task.json` status, meta, and gate extras are a same-command
|
|
5
|
+
projection, not a second core-state writer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .log import Colors, colored
|
|
18
|
+
|
|
19
|
+
TASK_RECORD_FIELDS = (
|
|
20
|
+
"id",
|
|
21
|
+
"name",
|
|
22
|
+
"title",
|
|
23
|
+
"description",
|
|
24
|
+
"status",
|
|
25
|
+
"dev_type",
|
|
26
|
+
"scope",
|
|
27
|
+
"package",
|
|
28
|
+
"priority",
|
|
29
|
+
"creator",
|
|
30
|
+
"assignee",
|
|
31
|
+
"createdAt",
|
|
32
|
+
"completedAt",
|
|
33
|
+
"branch",
|
|
34
|
+
"base_branch",
|
|
35
|
+
"worktree_path",
|
|
36
|
+
"commit",
|
|
37
|
+
"pr_url",
|
|
38
|
+
"subtasks",
|
|
39
|
+
"children",
|
|
40
|
+
"parent",
|
|
41
|
+
"relatedFiles",
|
|
42
|
+
"notes",
|
|
43
|
+
"meta",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
_NULLABLE_STRING = {
|
|
47
|
+
"dev_type",
|
|
48
|
+
"scope",
|
|
49
|
+
"package",
|
|
50
|
+
"completedAt",
|
|
51
|
+
"branch",
|
|
52
|
+
"base_branch",
|
|
53
|
+
"worktree_path",
|
|
54
|
+
"commit",
|
|
55
|
+
"pr_url",
|
|
56
|
+
"parent",
|
|
57
|
+
}
|
|
58
|
+
_STRING_ARRAYS = {"subtasks", "children", "relatedFiles"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class KernelCliNotFound(RuntimeError):
|
|
62
|
+
"""`cstl kernel --json` (or TRELLIS_KERNEL_CLI) is not available."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class KernelCommandError(RuntimeError):
|
|
66
|
+
"""Kernel JSON CLI returned ok:false or could not be parsed."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
message: str,
|
|
71
|
+
*,
|
|
72
|
+
result: dict | None = None,
|
|
73
|
+
returncode: int | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
super().__init__(message)
|
|
76
|
+
self.result = result
|
|
77
|
+
self.returncode = returncode
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def to_kernel_record(task_data: dict) -> dict:
|
|
81
|
+
"""Project a Python task dict onto the canonical 24-field Kernel record."""
|
|
82
|
+
record: dict = {}
|
|
83
|
+
for field in TASK_RECORD_FIELDS:
|
|
84
|
+
if field in task_data:
|
|
85
|
+
record[field] = task_data[field]
|
|
86
|
+
continue
|
|
87
|
+
if field in _NULLABLE_STRING:
|
|
88
|
+
record[field] = None
|
|
89
|
+
elif field in _STRING_ARRAYS:
|
|
90
|
+
record[field] = []
|
|
91
|
+
elif field == "meta":
|
|
92
|
+
record[field] = {}
|
|
93
|
+
elif field == "priority":
|
|
94
|
+
record[field] = "P2"
|
|
95
|
+
elif field == "status":
|
|
96
|
+
record[field] = "planning"
|
|
97
|
+
elif field in ("description", "notes", "id", "name", "title", "creator", "assignee", "createdAt"):
|
|
98
|
+
record[field] = ""
|
|
99
|
+
else:
|
|
100
|
+
record[field] = None
|
|
101
|
+
if not isinstance(record["meta"], dict):
|
|
102
|
+
record["meta"] = {}
|
|
103
|
+
else:
|
|
104
|
+
record["meta"] = {
|
|
105
|
+
key: value
|
|
106
|
+
for key, value in record["meta"].items()
|
|
107
|
+
if key not in {"classification", "task_kind", "task_type", "mode"}
|
|
108
|
+
}
|
|
109
|
+
for array_field in _STRING_ARRAYS:
|
|
110
|
+
value = record[array_field]
|
|
111
|
+
if not isinstance(value, list):
|
|
112
|
+
record[array_field] = []
|
|
113
|
+
return record
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def kernel_cli_argv() -> list[str]:
|
|
117
|
+
override = os.environ.get("TRELLIS_KERNEL_CLI", "").strip()
|
|
118
|
+
if override:
|
|
119
|
+
if sys.platform == "win32":
|
|
120
|
+
import shlex
|
|
121
|
+
|
|
122
|
+
return shlex.split(override, posix=False)
|
|
123
|
+
import shlex
|
|
124
|
+
|
|
125
|
+
return shlex.split(override)
|
|
126
|
+
cstl = shutil.which("cstl")
|
|
127
|
+
if cstl:
|
|
128
|
+
return [cstl, "kernel", "--json"]
|
|
129
|
+
raise KernelCliNotFound(
|
|
130
|
+
"Kernel CLI not found. Set TRELLIS_KERNEL_CLI or put `cstl` on PATH. "
|
|
131
|
+
"Stage 2 create/start-execution/record-gate/archive/patch require `cstl kernel --json`."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_kernel_command(payload: dict, *, cwd: Path | None = None) -> dict:
|
|
136
|
+
"""Send one Kernel Command on stdin and parse one JSON object from stdout."""
|
|
137
|
+
argv = kernel_cli_argv()
|
|
138
|
+
encoded = json.dumps(payload, ensure_ascii=False)
|
|
139
|
+
try:
|
|
140
|
+
proc = subprocess.run(
|
|
141
|
+
argv,
|
|
142
|
+
input=encoded,
|
|
143
|
+
capture_output=True,
|
|
144
|
+
text=True,
|
|
145
|
+
encoding="utf-8",
|
|
146
|
+
cwd=str(cwd) if cwd is not None else None,
|
|
147
|
+
check=False,
|
|
148
|
+
)
|
|
149
|
+
except OSError as err:
|
|
150
|
+
raise KernelCommandError(f"failed to invoke Kernel CLI {argv!r}: {err}") from err
|
|
151
|
+
|
|
152
|
+
raw = (proc.stdout or "").strip()
|
|
153
|
+
parsed: dict | None = None
|
|
154
|
+
if raw:
|
|
155
|
+
try:
|
|
156
|
+
parsed = json.loads(raw)
|
|
157
|
+
except json.JSONDecodeError:
|
|
158
|
+
last_line = raw.splitlines()[-1] if raw.splitlines() else raw
|
|
159
|
+
try:
|
|
160
|
+
parsed = json.loads(last_line)
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
parsed = None
|
|
163
|
+
|
|
164
|
+
if not isinstance(parsed, dict):
|
|
165
|
+
err_tail = (proc.stderr or "").strip()[-800:]
|
|
166
|
+
raise KernelCommandError(
|
|
167
|
+
f"Kernel CLI did not return JSON (exit {proc.returncode}). {err_tail}",
|
|
168
|
+
returncode=proc.returncode,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if not parsed.get("ok"):
|
|
172
|
+
error = parsed.get("error") if isinstance(parsed.get("error"), dict) else {}
|
|
173
|
+
code = error.get("code", "UNKNOWN")
|
|
174
|
+
message = error.get("message", "kernel command failed")
|
|
175
|
+
raise KernelCommandError(
|
|
176
|
+
f"Kernel {payload.get('op', 'command')} failed ({code}): {message}",
|
|
177
|
+
result=parsed,
|
|
178
|
+
returncode=proc.returncode,
|
|
179
|
+
)
|
|
180
|
+
return parsed
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def kernel_read(task_dir: Path) -> dict:
|
|
184
|
+
return run_kernel_command({"op": "read", "taskDir": str(task_dir)})
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def kernel_expected_revision(task_dir: Path) -> int:
|
|
188
|
+
result = kernel_read(task_dir)
|
|
189
|
+
kernel = result.get("kernel")
|
|
190
|
+
if not isinstance(kernel, dict):
|
|
191
|
+
raise KernelCommandError("Kernel read did not include kernel.revision", result=result)
|
|
192
|
+
revision = kernel.get("revision")
|
|
193
|
+
if not isinstance(revision, int) or revision < 0:
|
|
194
|
+
raise KernelCommandError(
|
|
195
|
+
f"Kernel revision is invalid: {revision!r}",
|
|
196
|
+
result=result,
|
|
197
|
+
)
|
|
198
|
+
return revision
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def kernel_create(
|
|
202
|
+
task_dir: Path,
|
|
203
|
+
record: dict,
|
|
204
|
+
*,
|
|
205
|
+
actor: str,
|
|
206
|
+
idempotency_key: str,
|
|
207
|
+
evidence: str | None = None,
|
|
208
|
+
extras: dict | None = None,
|
|
209
|
+
) -> dict:
|
|
210
|
+
payload: dict = {
|
|
211
|
+
"op": "create",
|
|
212
|
+
"taskDir": str(task_dir),
|
|
213
|
+
"actor": actor,
|
|
214
|
+
"idempotencyKey": idempotency_key,
|
|
215
|
+
"record": to_kernel_record(record),
|
|
216
|
+
}
|
|
217
|
+
if extras:
|
|
218
|
+
payload["extras"] = extras
|
|
219
|
+
if evidence:
|
|
220
|
+
payload["evidence"] = evidence
|
|
221
|
+
return run_kernel_command(payload)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def kernel_start(
|
|
225
|
+
task_dir: Path,
|
|
226
|
+
record: dict,
|
|
227
|
+
extras: dict,
|
|
228
|
+
*,
|
|
229
|
+
expected_revision: int,
|
|
230
|
+
actor: str,
|
|
231
|
+
idempotency_key: str,
|
|
232
|
+
evidence: str | None = None,
|
|
233
|
+
) -> dict:
|
|
234
|
+
payload: dict = {
|
|
235
|
+
"op": "start",
|
|
236
|
+
"taskDir": str(task_dir),
|
|
237
|
+
"expectedRevision": expected_revision,
|
|
238
|
+
"actor": actor,
|
|
239
|
+
"idempotencyKey": idempotency_key,
|
|
240
|
+
"record": to_kernel_record(record),
|
|
241
|
+
"extras": extras,
|
|
242
|
+
}
|
|
243
|
+
if evidence:
|
|
244
|
+
payload["evidence"] = evidence
|
|
245
|
+
return run_kernel_command(payload)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def kernel_record_gate(
|
|
249
|
+
task_dir: Path,
|
|
250
|
+
*,
|
|
251
|
+
expected_revision: int,
|
|
252
|
+
actor: str,
|
|
253
|
+
idempotency_key: str,
|
|
254
|
+
transition: str,
|
|
255
|
+
gate: str,
|
|
256
|
+
record: dict,
|
|
257
|
+
extras: dict,
|
|
258
|
+
evidence: str | None = None,
|
|
259
|
+
) -> dict:
|
|
260
|
+
payload: dict = {
|
|
261
|
+
"op": "record-gate",
|
|
262
|
+
"taskDir": str(task_dir),
|
|
263
|
+
"expectedRevision": expected_revision,
|
|
264
|
+
"actor": actor,
|
|
265
|
+
"idempotencyKey": idempotency_key,
|
|
266
|
+
"transition": transition,
|
|
267
|
+
"gate": gate,
|
|
268
|
+
"record": record,
|
|
269
|
+
"extras": extras,
|
|
270
|
+
}
|
|
271
|
+
if evidence:
|
|
272
|
+
payload["evidence"] = evidence
|
|
273
|
+
return run_kernel_command(payload)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
KERNEL_PROJECTION_EXTRA_KEYS = (
|
|
277
|
+
"quality_gate_results",
|
|
278
|
+
"execution_approval",
|
|
279
|
+
"required_controls",
|
|
280
|
+
"ac_evidence_ledger",
|
|
281
|
+
"independent_check",
|
|
282
|
+
"topology",
|
|
283
|
+
"dependency_graph",
|
|
284
|
+
"ondemand_modules",
|
|
285
|
+
"depends_on",
|
|
286
|
+
"dependency_satisfied",
|
|
287
|
+
"decompose_proposal",
|
|
288
|
+
"profile_health",
|
|
289
|
+
"event_bridge",
|
|
290
|
+
"middleware_providers",
|
|
291
|
+
"capability_router",
|
|
292
|
+
"required_capabilities",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def kernel_projection_extras(task_data: dict) -> dict:
|
|
297
|
+
"""Non-canonical task.json fields owned by the same Kernel Command."""
|
|
298
|
+
extras: dict = {}
|
|
299
|
+
for key in KERNEL_PROJECTION_EXTRA_KEYS:
|
|
300
|
+
if key in task_data:
|
|
301
|
+
extras[key] = task_data[key]
|
|
302
|
+
return extras
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def kernel_archive(
|
|
306
|
+
task_dir: Path,
|
|
307
|
+
record: dict,
|
|
308
|
+
extras: dict,
|
|
309
|
+
*,
|
|
310
|
+
expected_revision: int,
|
|
311
|
+
actor: str,
|
|
312
|
+
idempotency_key: str,
|
|
313
|
+
evidence: str | None = None,
|
|
314
|
+
) -> dict:
|
|
315
|
+
payload: dict = {
|
|
316
|
+
"op": "archive",
|
|
317
|
+
"taskDir": str(task_dir),
|
|
318
|
+
"expectedRevision": expected_revision,
|
|
319
|
+
"actor": actor,
|
|
320
|
+
"idempotencyKey": idempotency_key,
|
|
321
|
+
"record": to_kernel_record(record),
|
|
322
|
+
"extras": extras,
|
|
323
|
+
}
|
|
324
|
+
if evidence:
|
|
325
|
+
payload["evidence"] = evidence
|
|
326
|
+
return run_kernel_command(payload)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def kernel_patch(
|
|
330
|
+
task_dir: Path,
|
|
331
|
+
record: dict | None = None,
|
|
332
|
+
extras: dict | None = None,
|
|
333
|
+
*,
|
|
334
|
+
expected_revision: int,
|
|
335
|
+
actor: str,
|
|
336
|
+
idempotency_key: str,
|
|
337
|
+
evidence: str | None = None,
|
|
338
|
+
) -> dict:
|
|
339
|
+
payload: dict = {
|
|
340
|
+
"op": "patch",
|
|
341
|
+
"taskDir": str(task_dir),
|
|
342
|
+
"expectedRevision": expected_revision,
|
|
343
|
+
"actor": actor,
|
|
344
|
+
"idempotencyKey": idempotency_key,
|
|
345
|
+
}
|
|
346
|
+
if record is not None:
|
|
347
|
+
payload["record"] = to_kernel_record(record)
|
|
348
|
+
if extras is not None:
|
|
349
|
+
payload["extras"] = extras
|
|
350
|
+
if evidence:
|
|
351
|
+
payload["evidence"] = evidence
|
|
352
|
+
return run_kernel_command(payload)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def print_kernel_error(err: Exception) -> None:
|
|
356
|
+
print(colored(f"Error: {err}", Colors.RED), file=sys.stderr)
|
|
357
|
+
if isinstance(err, KernelCommandError) and isinstance(err.result, dict):
|
|
358
|
+
half = err.result.get("halfConversion")
|
|
359
|
+
if isinstance(half, dict) and half.get("kernelPersisted"):
|
|
360
|
+
print(
|
|
361
|
+
colored(
|
|
362
|
+
"Kernel.json was committed but the task.json projection did not finish "
|
|
363
|
+
"(half-conversion). Re-run the same command to recover; do not treat this as success.",
|
|
364
|
+
Colors.YELLOW,
|
|
365
|
+
),
|
|
366
|
+
file=sys.stderr,
|
|
367
|
+
)
|