its-magic 0.1.2-9 → 0.1.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/README.md +1602 -755
- package/bin/its-magic.js +121 -11
- package/bin/postinstall.js +21 -0
- package/installer.ps1 +759 -0
- package/installer.py +981 -0
- package/installer.sh +649 -0
- package/package.json +18 -14
- package/scripts/check_intake_template_parity.py +284 -0
- package/scripts/doc_profile_lib.py +415 -0
- package/scripts/guard_installer_publish.py +88 -0
- package/scripts/intake_bug_routing_guard.py +67 -0
- package/scripts/intake_evidence_lib.py +802 -0
- package/scripts/intake_evidence_validate.py +73 -0
- package/scripts/materialize_codebase_map.py +184 -0
- package/scripts/remote_config_summary.py +243 -0
- package/template/.cursor/agents/curator.mdc +35 -0
- package/template/.cursor/agents/dev.mdc +29 -0
- package/template/.cursor/agents/po.mdc +141 -0
- package/template/.cursor/agents/qa.mdc +28 -0
- package/template/.cursor/agents/release.mdc +28 -0
- package/template/.cursor/agents/security.mdc +98 -0
- package/template/.cursor/agents/tech-lead.mdc +57 -0
- package/template/.cursor/commands/architecture.md +127 -0
- package/template/.cursor/commands/ask.md +55 -0
- package/template/.cursor/commands/auto.md +533 -0
- package/template/.cursor/commands/discovery.md +47 -0
- package/template/.cursor/commands/execute.md +343 -0
- package/template/.cursor/commands/intake.md +323 -0
- package/template/.cursor/commands/map-codebase.md +46 -0
- package/template/.cursor/commands/memory-audit.md +175 -0
- package/template/.cursor/commands/milestone-complete.md +51 -0
- package/template/.cursor/commands/milestone-start.md +59 -0
- package/template/.cursor/commands/pause.md +64 -0
- package/{.cursor/commands/gsd-phase-context.md → template/.cursor/commands/phase-context.md} +6 -2
- package/template/.cursor/commands/plan-verify.md +34 -0
- package/template/.cursor/commands/qa.md +226 -0
- package/template/.cursor/commands/quick.md +41 -0
- package/template/.cursor/commands/refresh-context.md +82 -0
- package/template/.cursor/commands/release.md +505 -0
- package/template/.cursor/commands/research.md +49 -0
- package/template/.cursor/commands/resume.md +67 -0
- package/template/.cursor/commands/security-review.md +81 -0
- package/template/.cursor/commands/sprint-plan.md +103 -0
- package/template/.cursor/commands/status-reconcile.md +95 -0
- package/template/.cursor/commands/verify-work.md +152 -0
- package/template/.cursor/dev-environment.json.example +22 -0
- package/{.cursor → template/.cursor}/hooks/README.md +3 -2
- package/{.cursor/hooks/gsd-hook.py → template/.cursor/hooks/hook.py} +15 -6
- package/template/.cursor/hooks.json +26 -0
- package/template/.cursor/remote.json +31 -0
- package/template/.cursor/rules/caveman.mdc +184 -0
- package/template/.cursor/rules/coding-standards.mdc +39 -0
- package/template/.cursor/rules/core.mdc +111 -0
- package/template/.cursor/rules/handoffs.mdc +27 -0
- package/template/.cursor/rules/quality.mdc +49 -0
- package/template/.cursor/scratchpad.local.example.md +323 -0
- package/template/.cursor/scratchpad.md +324 -0
- package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/SKILL.md +2 -2
- package/template/.cursorignore +6 -0
- package/template/.env.example +28 -0
- package/template/.github/workflows/ci.yml +194 -0
- package/{.github → template/.github}/workflows/deploy.yml +5 -2
- package/template/.its-magic-version +1 -0
- package/template/README.md +1602 -0
- package/template/decisions/DEC-0001.md +11 -0
- package/template/decisions/DEC-0002.md +11 -0
- package/template/docs/developer/README.md +44 -0
- package/template/docs/engineering/architecture.md +9 -0
- package/template/docs/engineering/artifact-ordering-policy.md +48 -0
- package/template/docs/engineering/artifact-ownership-policy.md +57 -0
- package/template/docs/engineering/auto-orchestration-reference.md +1211 -0
- package/{docs → template/docs}/engineering/codebase-map.md +0 -5
- package/template/docs/engineering/compatibility-report.md +20 -0
- package/template/docs/engineering/compatibility-signals.md +7 -0
- package/template/docs/engineering/component-scope-report.md +16 -0
- package/template/docs/engineering/component-scope.md +19 -0
- package/template/docs/engineering/context/installer-owned-paths.manifest +96 -0
- package/template/docs/engineering/context/readme-section-affinity.json +30 -0
- package/template/docs/engineering/decisions.md +16 -0
- package/template/docs/engineering/legacy-drift-audit.md +21 -0
- package/template/docs/engineering/manifests/components/api-gateway.manifest.yaml +12 -0
- package/template/docs/engineering/manifests/registry.manifest.yaml +20 -0
- package/template/docs/engineering/manifests/repo.manifest.yaml +11 -0
- package/template/docs/engineering/phase-context.md +16 -0
- package/template/docs/engineering/release-targets.json +123 -0
- package/template/docs/engineering/research.md +29 -0
- package/template/docs/engineering/runbook.md +2320 -0
- package/template/docs/engineering/runtime-connectivity.md +81 -0
- package/template/docs/engineering/security-review.md +33 -0
- package/template/docs/engineering/spec-pack/README.md +20 -0
- package/template/docs/engineering/state-archive/README.md +15 -0
- package/template/docs/engineering/state.md +20 -0
- package/template/docs/engineering/status-normalization-report.md +19 -0
- package/template/docs/engineering/token-cost-parity-manifest.md +16 -0
- package/template/docs/engineering/us-0084-remote-e2e.md +44 -0
- package/template/docs/product/acceptance.md +1 -0
- package/template/docs/product/backlog.md +5 -0
- package/template/docs/product/vision.md +11 -0
- package/template/docs/user-guides/README.md +40 -0
- package/{handoffs → template/handoffs}/dev_to_qa.md +0 -3
- package/{handoffs → template/handoffs}/po_to_tl.md +0 -3
- package/{handoffs → template/handoffs}/qa_to_dev.md +1 -2
- package/template/handoffs/release_notes.md +51 -0
- package/template/handoffs/release_queue.md +47 -0
- package/template/handoffs/release_to_dev.md +31 -0
- package/template/handoffs/releases/Sxxxx-release-notes.md +62 -0
- package/template/handoffs/resume_brief.md +14 -0
- package/{handoffs → template/handoffs}/tl_to_dev.md +0 -2
- package/template/handoffs/token_cost_runs/README.md +26 -0
- package/template/its_magic/.its-magic-version +1 -0
- package/template/its_magic/README.md +9 -0
- package/template/scripts/auto_outer_driver.py +521 -0
- package/template/scripts/caveman_compress_input.py +903 -0
- package/template/scripts/check_downstream_ci_guard.py +67 -0
- package/template/scripts/check_intake_template_parity.py +284 -0
- package/template/scripts/check_token_cost_parity.py +69 -0
- package/template/scripts/dev_environment_lib.py +463 -0
- package/template/scripts/doc_profile_lib.py +415 -0
- package/template/scripts/downstream_ci_guard_lib.py +222 -0
- package/template/scripts/enforce-triad-hot-surface.py +753 -0
- package/template/scripts/guard_installer_publish.py +88 -0
- package/template/scripts/intake_bug_resume_brief_refresh.py +303 -0
- package/template/scripts/intake_bug_routing_guard.py +67 -0
- package/template/scripts/intake_evidence_lib.py +802 -0
- package/template/scripts/intake_evidence_validate.py +73 -0
- package/template/scripts/materialize_codebase_map.py +184 -0
- package/template/scripts/pack_json_validate.py +130 -0
- package/template/scripts/project_readme_coverage_lib.py +417 -0
- package/template/scripts/readme_feature_coverage_lib.py +608 -0
- package/template/scripts/remote_config_summary.py +243 -0
- package/template/scripts/sync_push_gates.py +198 -0
- package/template/scripts/token_cost_compare.py +40 -0
- package/template/scripts/token_cost_lib.py +108 -0
- package/template/scripts/uat_probe_lib.py +868 -0
- package/template/scripts/validate-and-push.ps1 +280 -0
- package/template/scripts/validate-and-push.sh +243 -0
- package/template/scripts/validate_doc_profile.py +103 -0
- package/template/scripts/validate_project_readme_coverage.py +151 -0
- package/template/scripts/validate_readme_feature_coverage.py +140 -0
- package/template/sprints/S0001/progress.md +1 -0
- package/template/sprints/S0001/qa-findings.md +9 -0
- package/template/sprints/S0001/release-findings.md +24 -0
- package/template/sprints/S0001/sprint.md +9 -0
- package/template/sprints/S0001/summary.md +7 -0
- package/template/sprints/S0001/tasks.md +1 -0
- package/template/sprints/S0001/uat.json +6 -0
- package/template/sprints/S0001/uat.md +5 -0
- package/template/sprints/quick/Q0001/summary.md +1 -0
- package/{sprints → template/sprints}/quick/Q0001/task.json +0 -1
- package/.cursor/agents/curator.mdc +0 -21
- package/.cursor/agents/dev.mdc +0 -20
- package/.cursor/agents/po.mdc +0 -19
- package/.cursor/agents/qa.mdc +0 -19
- package/.cursor/agents/release.mdc +0 -19
- package/.cursor/agents/tech-lead.mdc +0 -21
- package/.cursor/commands/gsd-architecture.md +0 -29
- package/.cursor/commands/gsd-auto.md +0 -27
- package/.cursor/commands/gsd-discovery.md +0 -27
- package/.cursor/commands/gsd-execute.md +0 -32
- package/.cursor/commands/gsd-intake.md +0 -28
- package/.cursor/commands/gsd-map-codebase.md +0 -25
- package/.cursor/commands/gsd-milestone-complete.md +0 -24
- package/.cursor/commands/gsd-milestone-start.md +0 -26
- package/.cursor/commands/gsd-pause.md +0 -25
- package/.cursor/commands/gsd-plan-verify.md +0 -26
- package/.cursor/commands/gsd-qa.md +0 -28
- package/.cursor/commands/gsd-quick.md +0 -24
- package/.cursor/commands/gsd-refresh-context.md +0 -26
- package/.cursor/commands/gsd-release.md +0 -29
- package/.cursor/commands/gsd-research.md +0 -28
- package/.cursor/commands/gsd-resume.md +0 -26
- package/.cursor/commands/gsd-sprint-plan.md +0 -30
- package/.cursor/commands/gsd-verify-work.md +0 -25
- package/.cursor/hooks.json +0 -26
- package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +0 -57
- package/.cursor/remote.json +0 -18
- package/.cursor/rules/gsd-core.mdc +0 -18
- package/.cursor/rules/gsd-handoffs.mdc +0 -10
- package/.cursor/rules/gsd-quality.mdc +0 -15
- package/.cursor/scratchpad.md +0 -34
- package/.github/workflows/ci.yml +0 -47
- package/decisions/DEC-0001.md +0 -21
- package/decisions/DEC-0002.md +0 -21
- package/docs/engineering/architecture.md +0 -354
- package/docs/engineering/decisions.md +0 -6
- package/docs/engineering/research.md +0 -11
- package/docs/engineering/runbook.md +0 -32
- package/docs/engineering/state.md +0 -33
- package/docs/product/acceptance.md +0 -6
- package/docs/product/backlog.md +0 -7
- package/docs/product/vision.md +0 -46
- package/gsd-installer.ps1 +0 -189
- package/gsd-installer.py +0 -195
- package/gsd-installer.sh +0 -201
- package/handoffs/release_notes.md +0 -14
- package/handoffs/resume_brief.md +0 -8
- package/milestones/M0001/milestone.json +0 -7
- package/milestones/M0001/phases.json +0 -9
- package/milestones/M0001/progress.md +0 -3
- package/milestones/M0001/summary.md +0 -3
- package/scripts/generate-release-notes.ps1 +0 -74
- package/scripts/generate-release-notes.sh +0 -63
- package/scripts/release-all.ps1 +0 -423
- package/scripts/release-all.sh +0 -226
- package/sprints/S0001/progress.md +0 -4
- package/sprints/S0001/qa-findings.md +0 -113
- package/sprints/S0001/sprint.md +0 -70
- package/sprints/S0001/summary.md +0 -46
- package/sprints/S0001/tasks.md +0 -35
- package/sprints/S0001/uat.json +0 -8
- package/sprints/S0001/uat.md +0 -8
- package/sprints/quick/Q0001/summary.md +0 -3
- /package/{.cursor/rules/gsd-escalation.mdc → template/.cursor/rules/escalation.mdc} +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/acceptance.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/architecture.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/decision.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/handoff.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/milestone.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/phase-context.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/plan-verify.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/sprint.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.json +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/story.md +0 -0
- /package/{.cursor/skills/gsd-team → template/.cursor/skills/its-magic}/templates/uat.json +0 -0
- /package/{docs → template/docs}/engineering/context/phase-template.json +0 -0
- /package/{docs → template/docs}/engineering/dependencies.json +0 -0
- /package/{sprints → template/sprints}/S0001/plan-verify.json +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Summarize .cursor/remote.json for operators (US-0084 / US-0064 alignment).
|
|
4
|
+
|
|
5
|
+
Stdout: non-secret labels and env-reference names only (no key material).
|
|
6
|
+
Stderr: errors and skip reasons.
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
0 OK or REMOTE_EXECUTION=0 skip (DEC-0070)
|
|
10
|
+
1 usage / CLI error
|
|
11
|
+
2 config missing or unreadable
|
|
12
|
+
3 invalid JSON
|
|
13
|
+
4 schema / contract mismatch vs runbook remote.json contract
|
|
14
|
+
5 reserved (unused; DEC-0070 maps REMOTE_EXECUTION=0 to 0)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
ENV_VAR_NAME = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
28
|
+
ALLOWED_TYPES = frozenset({"docker", "ssh", "vm"})
|
|
29
|
+
AUTH_MODES = frozenset({"none", "env"})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _truthy_remote_execution(raw: str | None) -> bool:
|
|
33
|
+
if raw is None:
|
|
34
|
+
return False
|
|
35
|
+
return raw.strip() in {"1", "true", "TRUE", "yes", "YES", "on", "ON"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
|
|
39
|
+
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], add_help=True)
|
|
40
|
+
p.add_argument(
|
|
41
|
+
"--config",
|
|
42
|
+
default=None,
|
|
43
|
+
help="Path to remote JSON (default: REMOTE_CONFIG env or .cursor/remote.json)",
|
|
44
|
+
)
|
|
45
|
+
return p.parse_known_args(argv)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _config_path(explicit: str | None) -> Path:
|
|
49
|
+
if explicit:
|
|
50
|
+
return Path(explicit).expanduser()
|
|
51
|
+
env = os.environ.get("REMOTE_CONFIG")
|
|
52
|
+
if env:
|
|
53
|
+
return Path(env).expanduser()
|
|
54
|
+
return Path(".cursor/remote.json")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate_env_ref(name: str, value: Any, prefix: str) -> str | None:
|
|
58
|
+
if not isinstance(value, str):
|
|
59
|
+
return f"{prefix}: {name} must be a string env-reference name"
|
|
60
|
+
if not ENV_VAR_NAME.match(value):
|
|
61
|
+
return f"{prefix}: {name} must look like an env var name (A-Z_0-9)"
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def validate_and_summarize(path: Path, data: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
66
|
+
"""Return (stdout_lines, error_messages)."""
|
|
67
|
+
errs: list[str] = []
|
|
68
|
+
out: list[str] = []
|
|
69
|
+
out.append(f"remote_config={path.as_posix()}")
|
|
70
|
+
|
|
71
|
+
if not isinstance(data.get("version"), int):
|
|
72
|
+
errs.append("[REMOTE_CONFIG_ERROR] root.version: expected integer")
|
|
73
|
+
if not isinstance(data.get("defaultTarget"), str) or not data["defaultTarget"]:
|
|
74
|
+
errs.append("[REMOTE_CONFIG_ERROR] root.defaultTarget: expected non-empty string")
|
|
75
|
+
targets = data.get("targets")
|
|
76
|
+
if not isinstance(targets, list) or not targets:
|
|
77
|
+
errs.append("[REMOTE_CONFIG_ERROR] root.targets: expected non-empty array")
|
|
78
|
+
|
|
79
|
+
if errs:
|
|
80
|
+
return out, errs
|
|
81
|
+
|
|
82
|
+
by_id: dict[str, dict[str, Any]] = {}
|
|
83
|
+
for i, t in enumerate(targets):
|
|
84
|
+
prefix = f"targets[{i}]"
|
|
85
|
+
if not isinstance(t, dict):
|
|
86
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}: expected object")
|
|
87
|
+
continue
|
|
88
|
+
tid = t.get("id")
|
|
89
|
+
if not isinstance(tid, str) or not tid:
|
|
90
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.id: expected non-empty string")
|
|
91
|
+
ttype = t.get("type")
|
|
92
|
+
if ttype not in ALLOWED_TYPES:
|
|
93
|
+
errs.append(
|
|
94
|
+
f"[REMOTE_CONFIG_ERROR] {prefix}.type: expected one of {sorted(ALLOWED_TYPES)}"
|
|
95
|
+
)
|
|
96
|
+
if not isinstance(t.get("enabled"), bool):
|
|
97
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.enabled: expected boolean")
|
|
98
|
+
if not isinstance(t.get("host"), str) or not t["host"]:
|
|
99
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.host: expected non-empty string")
|
|
100
|
+
port = t.get("port")
|
|
101
|
+
if not isinstance(port, int) or not (1 <= port <= 65535):
|
|
102
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.port: expected integer 1..65535")
|
|
103
|
+
if not isinstance(t.get("workspaceRoot"), str) or not t["workspaceRoot"]:
|
|
104
|
+
errs.append(
|
|
105
|
+
f"[REMOTE_CONFIG_ERROR] {prefix}.workspaceRoot: expected non-empty string"
|
|
106
|
+
)
|
|
107
|
+
auth = t.get("auth")
|
|
108
|
+
if auth is not None:
|
|
109
|
+
if not isinstance(auth, dict):
|
|
110
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {prefix}.auth: expected object")
|
|
111
|
+
else:
|
|
112
|
+
mode = auth.get("mode")
|
|
113
|
+
if mode not in AUTH_MODES:
|
|
114
|
+
errs.append(
|
|
115
|
+
f"[REMOTE_CONFIG_ERROR] {prefix}.auth.mode: expected one of {sorted(AUTH_MODES)}"
|
|
116
|
+
)
|
|
117
|
+
elif mode == "env":
|
|
118
|
+
env_keys = (
|
|
119
|
+
"tokenEnv",
|
|
120
|
+
"passwordEnv",
|
|
121
|
+
"privateKeyPathEnv",
|
|
122
|
+
"usernameEnv",
|
|
123
|
+
)
|
|
124
|
+
any_ref = False
|
|
125
|
+
for k in env_keys:
|
|
126
|
+
if k not in auth:
|
|
127
|
+
continue
|
|
128
|
+
any_ref = True
|
|
129
|
+
err = _validate_env_ref(k, auth[k], f"{prefix}.auth")
|
|
130
|
+
if err:
|
|
131
|
+
errs.append(f"[REMOTE_CONFIG_ERROR] {err}")
|
|
132
|
+
if not any_ref:
|
|
133
|
+
errs.append(
|
|
134
|
+
f"[REMOTE_CONFIG_ERROR] {prefix}.auth: mode=env requires at least one *Env field"
|
|
135
|
+
)
|
|
136
|
+
if isinstance(tid, str) and tid:
|
|
137
|
+
by_id[tid] = t
|
|
138
|
+
|
|
139
|
+
default = data["defaultTarget"]
|
|
140
|
+
if default not in by_id:
|
|
141
|
+
errs.append(
|
|
142
|
+
f"[REMOTE_CONFIG_ERROR] defaultTarget={default!r} not found in targets[].id"
|
|
143
|
+
)
|
|
144
|
+
elif not by_id[default].get("enabled"):
|
|
145
|
+
errs.append(
|
|
146
|
+
f"[REMOTE_CONFIG_ERROR] defaultTarget={default!r} must reference an enabled target"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if errs:
|
|
150
|
+
return out, errs
|
|
151
|
+
|
|
152
|
+
out.append(f"defaultTarget={data['defaultTarget']}")
|
|
153
|
+
for t in targets:
|
|
154
|
+
assert isinstance(t, dict)
|
|
155
|
+
tid = t["id"]
|
|
156
|
+
parts = [
|
|
157
|
+
f"id={tid}",
|
|
158
|
+
f"type={t['type']}",
|
|
159
|
+
f"enabled={t['enabled']!s}",
|
|
160
|
+
f"host={t['host']}",
|
|
161
|
+
f"port={t['port']}",
|
|
162
|
+
f"workspaceRoot={t['workspaceRoot']}",
|
|
163
|
+
]
|
|
164
|
+
auth = t.get("auth")
|
|
165
|
+
if isinstance(auth, dict):
|
|
166
|
+
parts.append(f"auth.mode={auth.get('mode')}")
|
|
167
|
+
for k in (
|
|
168
|
+
"tokenEnv",
|
|
169
|
+
"passwordEnv",
|
|
170
|
+
"privateKeyPathEnv",
|
|
171
|
+
"usernameEnv",
|
|
172
|
+
):
|
|
173
|
+
if k in auth and isinstance(auth[k], str):
|
|
174
|
+
parts.append(f"auth.{k}={auth[k]}")
|
|
175
|
+
out.append("target:" + " ".join(parts))
|
|
176
|
+
|
|
177
|
+
return out, []
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main(argv: list[str] | None = None) -> int:
|
|
181
|
+
args, rest = _parse_args(argv or sys.argv[1:])
|
|
182
|
+
if rest:
|
|
183
|
+
print(
|
|
184
|
+
f"[REMOTE_CONFIG_SUMMARY] unknown arguments: {' '.join(rest)}",
|
|
185
|
+
file=sys.stderr,
|
|
186
|
+
)
|
|
187
|
+
return 1
|
|
188
|
+
if not _truthy_remote_execution(os.environ.get("REMOTE_EXECUTION")):
|
|
189
|
+
print(
|
|
190
|
+
"[REMOTE_CONFIG_SUMMARY] REMOTE_EXECUTION!=1: skip validation/summary "
|
|
191
|
+
"(zero overhead; DEC-0070 / US-0084).",
|
|
192
|
+
file=sys.stderr,
|
|
193
|
+
)
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
path = _config_path(args.config)
|
|
197
|
+
if not path.is_file():
|
|
198
|
+
print(
|
|
199
|
+
f"[REMOTE_CONFIG_ERROR] {path}: file missing or unreadable. "
|
|
200
|
+
"Fix: create config or set REMOTE_EXECUTION=0.",
|
|
201
|
+
file=sys.stderr,
|
|
202
|
+
)
|
|
203
|
+
return 2
|
|
204
|
+
try:
|
|
205
|
+
raw = path.read_text(encoding="utf-8")
|
|
206
|
+
except OSError as e:
|
|
207
|
+
print(
|
|
208
|
+
f"[REMOTE_CONFIG_ERROR] {path}: read failed ({e}). Fix: permissions/path.",
|
|
209
|
+
file=sys.stderr,
|
|
210
|
+
)
|
|
211
|
+
return 2
|
|
212
|
+
try:
|
|
213
|
+
data = json.loads(raw)
|
|
214
|
+
except json.JSONDecodeError as e:
|
|
215
|
+
print(
|
|
216
|
+
f"[REMOTE_CONFIG_ERROR] {path}: invalid JSON ({e}). Fix: syntax.",
|
|
217
|
+
file=sys.stderr,
|
|
218
|
+
)
|
|
219
|
+
return 3
|
|
220
|
+
if not isinstance(data, dict):
|
|
221
|
+
print(
|
|
222
|
+
f"[REMOTE_CONFIG_ERROR] {path}: expected JSON object at root.",
|
|
223
|
+
file=sys.stderr,
|
|
224
|
+
)
|
|
225
|
+
return 4
|
|
226
|
+
|
|
227
|
+
lines, errs = validate_and_summarize(path, data)
|
|
228
|
+
if errs:
|
|
229
|
+
for line in errs:
|
|
230
|
+
print(line, file=sys.stderr)
|
|
231
|
+
print(
|
|
232
|
+
f"[REMOTE_CONFIG_ERROR] {path}: contract check failed. "
|
|
233
|
+
"Fix: align with docs/engineering/runbook.md remote contract (US-0084).",
|
|
234
|
+
file=sys.stderr,
|
|
235
|
+
)
|
|
236
|
+
return 4
|
|
237
|
+
for line in lines:
|
|
238
|
+
print(line)
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared sync / push eligibility evaluation for validate-and-push (merged scratchpad).
|
|
4
|
+
|
|
5
|
+
Imports installer.merge_scratchpad_layers / validate_merged_scratchpad only — no
|
|
6
|
+
duplicate DEC-0055 precedence logic. Emits machine-readable JSON on stdout; reason
|
|
7
|
+
codes only (no planning-shaped tokens) on stderr for errors.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import fnmatch
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Tuple
|
|
20
|
+
|
|
21
|
+
# Repo root (parent of scripts/)
|
|
22
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
23
|
+
if str(_ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(_ROOT))
|
|
25
|
+
|
|
26
|
+
import installer # noqa: E402
|
|
27
|
+
|
|
28
|
+
_VALID_MODES = frozenset(
|
|
29
|
+
{"disabled", "manual", "by_phase", "by_milestone", "custom_phase_list"}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _parse_csv(val: str) -> List[str]:
|
|
33
|
+
if not val or not val.strip():
|
|
34
|
+
return []
|
|
35
|
+
return [p.strip() for p in val.split(",") if p.strip()]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _merge_and_validate(root: str) -> Tuple[dict, List[str]]:
|
|
39
|
+
ok, diags = installer.validate_merged_scratchpad(root)
|
|
40
|
+
if not ok:
|
|
41
|
+
return {}, diags
|
|
42
|
+
merged, _paths = installer.merge_scratchpad_layers(root)
|
|
43
|
+
return merged, []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def eval_policy(root: str, _branch: str) -> Tuple[bool, str, List[str]]:
|
|
47
|
+
"""
|
|
48
|
+
Pre-test gate: merged scratchpad validation + SYNC_POLICY_MODE + ALLOW_AUTO_PUSH
|
|
49
|
+
+ custom_phase_list boundary (SYNC_PHASE_BOUNDARY env).
|
|
50
|
+
Returns (ok, reason_code, scratchpad_diagnostics).
|
|
51
|
+
"""
|
|
52
|
+
merged, diags = _merge_and_validate(root)
|
|
53
|
+
if diags:
|
|
54
|
+
return False, "SCRATCHPAD_MERGE_ERROR", diags
|
|
55
|
+
|
|
56
|
+
mode = (merged.get("SYNC_POLICY_MODE") or "").strip().lower()
|
|
57
|
+
if not mode or mode not in _VALID_MODES:
|
|
58
|
+
mode = "manual"
|
|
59
|
+
if mode == "disabled":
|
|
60
|
+
return False, "SYNC_DISABLED", []
|
|
61
|
+
if mode == "manual":
|
|
62
|
+
return False, "MANUAL_MODE_NO_AUTO", []
|
|
63
|
+
|
|
64
|
+
allow = (merged.get("ALLOW_AUTO_PUSH") or "").strip()
|
|
65
|
+
if allow != "1":
|
|
66
|
+
return False, "AUTO_PUSH_NOT_ENABLED", []
|
|
67
|
+
|
|
68
|
+
if mode == "custom_phase_list":
|
|
69
|
+
phases = [p.strip().lower() for p in _parse_csv(merged.get("SYNC_CUSTOM_PHASES", ""))]
|
|
70
|
+
boundary = (os.environ.get("SYNC_PHASE_BOUNDARY") or "").strip().lower()
|
|
71
|
+
if not boundary:
|
|
72
|
+
return False, "SYNC_TRIGGER_NOT_ELIGIBLE", []
|
|
73
|
+
if not phases or boundary not in phases:
|
|
74
|
+
return False, "SYNC_TRIGGER_NOT_ELIGIBLE", []
|
|
75
|
+
|
|
76
|
+
# by_phase / by_milestone: invocation counts as eligible boundary (DEC-0058 §4).
|
|
77
|
+
return True, "", []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _branch_allowed(branch: str, allowlist_csv: str) -> bool:
|
|
81
|
+
patterns = _parse_csv(allowlist_csv)
|
|
82
|
+
if not patterns:
|
|
83
|
+
return False
|
|
84
|
+
for pat in patterns:
|
|
85
|
+
if fnmatch.fnmatchcase(branch, pat):
|
|
86
|
+
return True
|
|
87
|
+
if branch == pat:
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _scan_qa_findings_blocking(repo: Path) -> bool:
|
|
93
|
+
"""True if blocking markers found (DEC-0058 §6)."""
|
|
94
|
+
sprints = repo / "sprints"
|
|
95
|
+
if not sprints.is_dir():
|
|
96
|
+
return False
|
|
97
|
+
for p in sorted(sprints.iterdir()):
|
|
98
|
+
if not p.is_dir() or len(p.name) != 5 or not p.name.startswith("S"):
|
|
99
|
+
continue
|
|
100
|
+
if not p.name[1:].isdigit():
|
|
101
|
+
continue
|
|
102
|
+
qf = p / "qa-findings.md"
|
|
103
|
+
if not qf.is_file():
|
|
104
|
+
continue
|
|
105
|
+
text = qf.read_text(encoding="utf-8", errors="replace")
|
|
106
|
+
if "BLOCKING_QA_FINDINGS" in text:
|
|
107
|
+
return True
|
|
108
|
+
in_blocking = False
|
|
109
|
+
for line in text.splitlines():
|
|
110
|
+
if re.match(r"^##\s+blocking\s*$", line, re.IGNORECASE):
|
|
111
|
+
in_blocking = True
|
|
112
|
+
continue
|
|
113
|
+
if re.match(r"^##\s+\S", line):
|
|
114
|
+
in_blocking = False
|
|
115
|
+
if in_blocking and re.search(r"^-\s+\[\s+\]", line):
|
|
116
|
+
return True
|
|
117
|
+
if re.search(r"^-\s+\[\s+\]", line) and re.search(
|
|
118
|
+
r"BLOCKING|FAIL", line, re.IGNORECASE
|
|
119
|
+
):
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _count_sprint_qa_findings(repo: Path) -> int:
|
|
125
|
+
n = 0
|
|
126
|
+
sprints = repo / "sprints"
|
|
127
|
+
if not sprints.is_dir():
|
|
128
|
+
return 0
|
|
129
|
+
for p in sprints.iterdir():
|
|
130
|
+
if not p.is_dir() or len(p.name) != 5 or not p.name.startswith("S"):
|
|
131
|
+
continue
|
|
132
|
+
if not p.name[1:].isdigit():
|
|
133
|
+
continue
|
|
134
|
+
if (p / "qa-findings.md").is_file():
|
|
135
|
+
n += 1
|
|
136
|
+
return n
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def eval_post_test(root: str, branch: str) -> Tuple[bool, str, List[str]]:
|
|
140
|
+
"""
|
|
141
|
+
After tests: branch allowlist + PRE_QA + BLOCKING_QA_FINDINGS scan.
|
|
142
|
+
"""
|
|
143
|
+
merged, diags = _merge_and_validate(root)
|
|
144
|
+
if diags:
|
|
145
|
+
return False, "SCRATCHPAD_MERGE_ERROR", diags
|
|
146
|
+
|
|
147
|
+
allow_csv = merged.get("AUTO_PUSH_BRANCH_ALLOWLIST") or ""
|
|
148
|
+
if not _branch_allowed(branch, allow_csv):
|
|
149
|
+
return False, "BRANCH_NOT_ALLOWLISTED", []
|
|
150
|
+
|
|
151
|
+
b = branch.strip()
|
|
152
|
+
if b not in ("main", "master"):
|
|
153
|
+
if _count_sprint_qa_findings(Path(root)) == 0:
|
|
154
|
+
return False, "PRE_QA_AUTOPUSH_FORBIDDEN", []
|
|
155
|
+
|
|
156
|
+
if _scan_qa_findings_blocking(Path(root)):
|
|
157
|
+
return False, "BLOCKING_QA_FINDINGS", []
|
|
158
|
+
|
|
159
|
+
return True, "", []
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main() -> int:
|
|
163
|
+
parser = argparse.ArgumentParser(description="Sync/push gate evaluation for validate-and-push.")
|
|
164
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
165
|
+
|
|
166
|
+
p_pol = sub.add_parser("policy", help="Pre-test merged scratchpad + sync policy gate.")
|
|
167
|
+
p_pol.add_argument("--root", required=True)
|
|
168
|
+
p_pol.add_argument("--branch", required=True)
|
|
169
|
+
|
|
170
|
+
p_post = sub.add_parser("post", help="Post-test allowlist + QA scan gate.")
|
|
171
|
+
p_post.add_argument("--root", required=True)
|
|
172
|
+
p_post.add_argument("--branch", required=True)
|
|
173
|
+
|
|
174
|
+
args = parser.parse_args()
|
|
175
|
+
root = os.path.abspath(args.root)
|
|
176
|
+
|
|
177
|
+
if args.cmd == "policy":
|
|
178
|
+
ok, reason, diags = eval_policy(root, args.branch)
|
|
179
|
+
if diags:
|
|
180
|
+
for d in diags:
|
|
181
|
+
print(d, file=sys.stderr)
|
|
182
|
+
print(json.dumps({"ok": False, "reason_code": "SCRATCHPAD_MERGE_ERROR"}))
|
|
183
|
+
return 2
|
|
184
|
+
print(json.dumps({"ok": ok, "reason_code": reason or None}))
|
|
185
|
+
return 0 if ok else 2
|
|
186
|
+
|
|
187
|
+
ok, reason, pdiags = eval_post_test(root, args.branch)
|
|
188
|
+
if pdiags:
|
|
189
|
+
for d in pdiags:
|
|
190
|
+
print(d, file=sys.stderr)
|
|
191
|
+
print(json.dumps({"ok": False, "reason_code": "SCRATCHPAD_MERGE_ERROR"}))
|
|
192
|
+
return 2
|
|
193
|
+
print(json.dumps({"ok": ok, "reason_code": reason or None}))
|
|
194
|
+
return 0 if ok else 2
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare two token-cost run JSON records for AC-2 (same run_class_hash, 50% cache read)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
if _SCRIPT_DIR not in sys.path:
|
|
14
|
+
sys.path.insert(0, _SCRIPT_DIR)
|
|
15
|
+
|
|
16
|
+
import token_cost_lib # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
21
|
+
p.add_argument("baseline", type=Path, help="JSON file with run_class_hash + totals")
|
|
22
|
+
p.add_argument("target", type=Path, help="JSON file with run_class_hash + totals")
|
|
23
|
+
p.add_argument(
|
|
24
|
+
"--reduction-fraction",
|
|
25
|
+
type=float,
|
|
26
|
+
default=0.5,
|
|
27
|
+
help="Required fractional reduction in cache_read_tokens (default 0.5)",
|
|
28
|
+
)
|
|
29
|
+
args = p.parse_args()
|
|
30
|
+
base = json.loads(args.baseline.read_text(encoding="utf-8"))
|
|
31
|
+
tgt = json.loads(args.target.read_text(encoding="utf-8"))
|
|
32
|
+
ok, msg = token_cost_lib.compare_cache_read_reduction(
|
|
33
|
+
base, tgt, reduction_fraction=args.reduction_fraction
|
|
34
|
+
)
|
|
35
|
+
print(msg)
|
|
36
|
+
return 0 if ok else 2
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sys.exit(main())
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Token-cost run class, metrics, and AC-2 comparability helpers (US-0080 / DEC-0062)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
# Canonical metric keys (DEC-0062 §1) — totals / per-phase rows
|
|
10
|
+
METRIC_KEYS = (
|
|
11
|
+
"cache_read_tokens",
|
|
12
|
+
"input_tokens",
|
|
13
|
+
"output_tokens",
|
|
14
|
+
"phase_call_count",
|
|
15
|
+
"cache_creation_tokens",
|
|
16
|
+
"orchestrator_call_estimate",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
REQUIRED_TOTAL_KEYS = (
|
|
20
|
+
"cache_read_tokens",
|
|
21
|
+
"input_tokens",
|
|
22
|
+
"output_tokens",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def canonical_json_dumps(obj: Mapping[str, Any]) -> str:
|
|
27
|
+
return json.dumps(obj, sort_keys=True, separators=(",", ":"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compute_run_class_hash(run_class: Mapping[str, Any]) -> str:
|
|
31
|
+
"""SHA-256 hex (lowercase) of canonical JSON for DEC-0062 §2 tuple."""
|
|
32
|
+
payload = canonical_json_dumps(run_class)
|
|
33
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def compute_strict_proof_hash(
|
|
37
|
+
orchestrator_run_id: str,
|
|
38
|
+
runtime_proof_id: str,
|
|
39
|
+
phase_id: str,
|
|
40
|
+
role: str,
|
|
41
|
+
proof_issued_at: str,
|
|
42
|
+
proof_ttl_seconds: int,
|
|
43
|
+
) -> str:
|
|
44
|
+
"""DEC-0038-style sorted-key JSON SHA-256 over the strict-proof tuple fields."""
|
|
45
|
+
obj = {
|
|
46
|
+
"orchestrator_run_id": orchestrator_run_id,
|
|
47
|
+
"runtime_proof_id": runtime_proof_id,
|
|
48
|
+
"phase_id": phase_id,
|
|
49
|
+
"role": role,
|
|
50
|
+
"proof_issued_at": proof_issued_at,
|
|
51
|
+
"proof_ttl_seconds": int(proof_ttl_seconds),
|
|
52
|
+
}
|
|
53
|
+
return hashlib.sha256(canonical_json_dumps(obj).encode("utf-8")).hexdigest()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_run_totals(row: Mapping[str, Any]) -> list[str]:
|
|
57
|
+
"""Return diagnostic strings for invalid totals; empty if OK."""
|
|
58
|
+
errs: list[str] = []
|
|
59
|
+
for k in REQUIRED_TOTAL_KEYS:
|
|
60
|
+
if k not in row:
|
|
61
|
+
errs.append(f"missing_metric:{k}")
|
|
62
|
+
continue
|
|
63
|
+
v = row[k]
|
|
64
|
+
if not isinstance(v, int) or v < 0:
|
|
65
|
+
errs.append(f"invalid_non_negative_int:{k}")
|
|
66
|
+
if "metric_source" not in row or not str(row.get("metric_source", "")).strip():
|
|
67
|
+
errs.append("missing_metric_source")
|
|
68
|
+
return errs
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def compare_cache_read_reduction(
|
|
72
|
+
baseline: Mapping[str, Any],
|
|
73
|
+
target: Mapping[str, Any],
|
|
74
|
+
*,
|
|
75
|
+
reduction_fraction: float = 0.5,
|
|
76
|
+
) -> tuple[bool, str]:
|
|
77
|
+
"""
|
|
78
|
+
AC-2: target must achieve >= reduction_fraction lower cache_read_tokens
|
|
79
|
+
vs baseline when run_class_hash matches. Otherwise TOKEN_COST_RUN_CLASS_MISMATCH.
|
|
80
|
+
"""
|
|
81
|
+
hb = baseline.get("run_class_hash")
|
|
82
|
+
ht = target.get("run_class_hash")
|
|
83
|
+
if not isinstance(hb, str) or not isinstance(ht, str) or hb != ht:
|
|
84
|
+
return False, "TOKEN_COST_RUN_CLASS_MISMATCH: run_class_hash differs or missing"
|
|
85
|
+
|
|
86
|
+
tb = baseline.get("totals") or {}
|
|
87
|
+
tt = target.get("totals") or {}
|
|
88
|
+
if not isinstance(tb, Mapping) or not isinstance(tt, Mapping):
|
|
89
|
+
return False, "TOKEN_COST_RUN_CLASS_MISMATCH: totals object missing"
|
|
90
|
+
|
|
91
|
+
br = tb.get("cache_read_tokens")
|
|
92
|
+
tr = tt.get("cache_read_tokens")
|
|
93
|
+
if not isinstance(br, int) or not isinstance(tr, int) or br < 0 or tr < 0:
|
|
94
|
+
return False, "TOKEN_COST_RUN_CLASS_MISMATCH: invalid cache_read_tokens"
|
|
95
|
+
|
|
96
|
+
if br == 0:
|
|
97
|
+
if tr == 0:
|
|
98
|
+
return True, "ok: baseline and target zero cache_read_tokens"
|
|
99
|
+
return False, "TOKEN_COST_RUN_CLASS_MISMATCH: baseline zero but target non-zero"
|
|
100
|
+
|
|
101
|
+
max_target = int(br * (1.0 - reduction_fraction))
|
|
102
|
+
if tr > max_target:
|
|
103
|
+
return (
|
|
104
|
+
False,
|
|
105
|
+
f"TOKEN_COST_RUN_CLASS_MISMATCH: cache_read_tokens {tr} exceeds "
|
|
106
|
+
f"50% reduction threshold (baseline {br}, max_target {max_target})",
|
|
107
|
+
)
|
|
108
|
+
return True, "ok: cache_read_tokens meets 50% reduction vs baseline"
|