@opengsd/gsd-path 1.6.0 → 1.6.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/AGENTS.md +1 -1
- package/CHANGELOG.md +44 -0
- package/FULL.md +5 -4
- package/HOOKS.md +18 -2
- package/QUICK.md +1 -1
- package/README.md +12 -28
- package/WORKFLOW.md +8 -4
- package/package.json +1 -1
- package/scripts/_common.py +43 -0
- package/scripts/check_task_briefs.py +10 -7
- package/scripts/dispatch_driver.py +17 -5
- package/scripts/git_guard.py +18 -2
- package/scripts/integration.py +17 -4
- package/scripts/isolation.py +66 -14
- package/scripts/lean_verification.py +5 -1
- package/scripts/model_policy.py +8 -1
- package/scripts/pipeline_git.py +19 -5
- package/scripts/pipeline_state.py +3 -1
- package/scripts/state_promote.py +77 -1
- package/skills/gsd-path/BUILD.md +3 -1
- package/skills/gsd-path/PLAN.md +3 -2
- package/skills/gsd-path/SHIP.md +14 -8
- package/skills/gsd-path/references/model-policy.md +3 -1
- package/skills/gsd-path/references/operating-rules.md +3 -2
- package/skills/gsd-path/references/runtime-selection.md +2 -1
- package/skills/gsd-path/scripts/_common.py +43 -0
- package/skills/gsd-path/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path/scripts/dispatch_driver.py +17 -5
- package/skills/gsd-path/scripts/git_guard.py +18 -2
- package/skills/gsd-path/scripts/integration.py +17 -4
- package/skills/gsd-path/scripts/isolation.py +66 -14
- package/skills/gsd-path/scripts/lean_verification.py +5 -1
- package/skills/gsd-path/scripts/model_policy.py +8 -1
- package/skills/gsd-path/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path/scripts/state_promote.py +77 -1
- package/skills/gsd-path-build/SKILL.md +3 -1
- package/skills/gsd-path-build/references/model-policy.md +3 -1
- package/skills/gsd-path-build/references/operating-rules.md +3 -2
- package/skills/gsd-path-build/references/runtime-selection.md +2 -1
- package/skills/gsd-path-build/scripts/_common.py +43 -0
- package/skills/gsd-path-build/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-build/scripts/dispatch_driver.py +17 -5
- package/skills/gsd-path-build/scripts/integration.py +17 -4
- package/skills/gsd-path-build/scripts/isolation.py +66 -14
- package/skills/gsd-path-build/scripts/model_policy.py +8 -1
- package/skills/gsd-path-build/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-build/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-build/scripts/state_promote.py +77 -1
- package/skills/gsd-path-decide/references/model-policy.md +3 -1
- package/skills/gsd-path-decide/references/operating-rules.md +3 -2
- package/skills/gsd-path-decide/references/runtime-selection.md +2 -1
- package/skills/gsd-path-decide/scripts/_common.py +43 -0
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-decide/scripts/integration.py +17 -4
- package/skills/gsd-path-decide/scripts/isolation.py +66 -14
- package/skills/gsd-path-decide/scripts/model_policy.py +8 -1
- package/skills/gsd-path-decide/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-decide/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-decide/scripts/state_promote.py +77 -1
- package/skills/gsd-path-define/references/operating-rules.md +3 -2
- package/skills/gsd-path-define/references/runtime-selection.md +2 -1
- package/skills/gsd-path-define/scripts/_common.py +43 -0
- package/skills/gsd-path-define/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-define/scripts/integration.py +17 -4
- package/skills/gsd-path-define/scripts/isolation.py +66 -14
- package/skills/gsd-path-define/scripts/model_policy.py +8 -1
- package/skills/gsd-path-define/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-define/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-define/scripts/state_promote.py +77 -1
- package/skills/gsd-path-discuss/references/operating-rules.md +3 -2
- package/skills/gsd-path-discuss/references/runtime-selection.md +2 -1
- package/skills/gsd-path-discuss/scripts/_common.py +43 -0
- package/skills/gsd-path-discuss/scripts/model_policy.py +8 -1
- package/skills/gsd-path-docs-audit/references/model-policy.md +3 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +3 -2
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +2 -1
- package/skills/gsd-path-docs-audit/scripts/_common.py +43 -0
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-docs-audit/scripts/integration.py +17 -4
- package/skills/gsd-path-docs-audit/scripts/isolation.py +66 -14
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +8 -1
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +77 -1
- package/skills/gsd-path-forensics/references/operating-rules.md +3 -2
- package/skills/gsd-path-forensics/references/runtime-selection.md +2 -1
- package/skills/gsd-path-forensics/scripts/_common.py +43 -0
- package/skills/gsd-path-forensics/scripts/model_policy.py +8 -1
- package/skills/gsd-path-inspect/references/model-policy.md +3 -1
- package/skills/gsd-path-inspect/references/operating-rules.md +3 -2
- package/skills/gsd-path-inspect/references/runtime-selection.md +2 -1
- package/skills/gsd-path-inspect/scripts/_common.py +43 -0
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-inspect/scripts/integration.py +17 -4
- package/skills/gsd-path-inspect/scripts/isolation.py +66 -14
- package/skills/gsd-path-inspect/scripts/model_policy.py +8 -1
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-inspect/scripts/state_promote.py +77 -1
- package/skills/gsd-path-loop/references/operating-rules.md +3 -2
- package/skills/gsd-path-loop/references/runtime-selection.md +2 -1
- package/skills/gsd-path-loop/scripts/_common.py +43 -0
- package/skills/gsd-path-loop/scripts/model_policy.py +8 -1
- package/skills/gsd-path-migrate/references/operating-rules.md +3 -2
- package/skills/gsd-path-migrate/references/runtime-selection.md +2 -1
- package/skills/gsd-path-migrate/scripts/_common.py +43 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +8 -1
- package/skills/gsd-path-plan/SKILL.md +3 -2
- package/skills/gsd-path-plan/references/model-policy.md +3 -1
- package/skills/gsd-path-plan/references/operating-rules.md +3 -2
- package/skills/gsd-path-plan/references/runtime-selection.md +2 -1
- package/skills/gsd-path-plan/scripts/_common.py +43 -0
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-plan/scripts/integration.py +17 -4
- package/skills/gsd-path-plan/scripts/isolation.py +66 -14
- package/skills/gsd-path-plan/scripts/model_policy.py +8 -1
- package/skills/gsd-path-plan/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-plan/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-plan/scripts/state_promote.py +77 -1
- package/skills/gsd-path-research/references/model-policy.md +3 -1
- package/skills/gsd-path-research/references/operating-rules.md +3 -2
- package/skills/gsd-path-research/references/runtime-selection.md +2 -1
- package/skills/gsd-path-research/scripts/_common.py +43 -0
- package/skills/gsd-path-research/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-research/scripts/integration.py +17 -4
- package/skills/gsd-path-research/scripts/isolation.py +66 -14
- package/skills/gsd-path-research/scripts/model_policy.py +8 -1
- package/skills/gsd-path-research/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-research/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-research/scripts/state_promote.py +77 -1
- package/skills/gsd-path-roadmap/references/model-policy.md +3 -1
- package/skills/gsd-path-roadmap/references/operating-rules.md +3 -2
- package/skills/gsd-path-roadmap/references/runtime-selection.md +2 -1
- package/skills/gsd-path-roadmap/scripts/_common.py +43 -0
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-roadmap/scripts/integration.py +17 -4
- package/skills/gsd-path-roadmap/scripts/isolation.py +66 -14
- package/skills/gsd-path-roadmap/scripts/model_policy.py +8 -1
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-roadmap/scripts/state_promote.py +77 -1
- package/skills/gsd-path-ship/SKILL.md +14 -8
- package/skills/gsd-path-ship/references/model-policy.md +3 -1
- package/skills/gsd-path-ship/references/operating-rules.md +3 -2
- package/skills/gsd-path-ship/references/runtime-selection.md +2 -1
- package/skills/gsd-path-ship/scripts/_common.py +43 -0
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +10 -7
- package/skills/gsd-path-ship/scripts/integration.py +17 -4
- package/skills/gsd-path-ship/scripts/isolation.py +66 -14
- package/skills/gsd-path-ship/scripts/lean_verification.py +5 -1
- package/skills/gsd-path-ship/scripts/model_policy.py +8 -1
- package/skills/gsd-path-ship/scripts/pipeline_git.py +19 -5
- package/skills/gsd-path-ship/scripts/pipeline_state.py +3 -1
- package/skills/gsd-path-ship/scripts/state_promote.py +77 -1
- package/skills/gsd-path-undo/references/operating-rules.md +3 -2
- package/skills/gsd-path-undo/references/runtime-selection.md +2 -1
- package/skills/gsd-path-undo/scripts/_common.py +43 -0
- package/skills/gsd-path-undo/scripts/model_policy.py +8 -1
- package/skills/path/BUILD.md +3 -1
- package/skills/path/PLAN.md +3 -2
- package/skills/path/SHIP.md +14 -8
- package/skills/path/references/model-policy.md +3 -1
- package/skills/path/references/operating-rules.md +3 -2
- package/skills/path/references/runtime-selection.md +2 -1
- package/skills/path/scripts/_common.py +43 -0
- package/skills/path/scripts/check_task_briefs.py +10 -7
- package/skills/path/scripts/dispatch_driver.py +17 -5
- package/skills/path/scripts/git_guard.py +18 -2
- package/skills/path/scripts/integration.py +17 -4
- package/skills/path/scripts/isolation.py +66 -14
- package/skills/path/scripts/lean_verification.py +5 -1
- package/skills/path/scripts/model_policy.py +8 -1
- package/skills/path/scripts/pipeline_git.py +19 -5
- package/skills/path/scripts/pipeline_state.py +3 -1
- package/skills/path/scripts/state_promote.py +77 -1
package/AGENTS.md
CHANGED
|
@@ -139,7 +139,7 @@ full-repo suite on a tiny edit outrank the phase brief.
|
|
|
139
139
|
canonical contents, carry-forward, and manifest pass the bundled precommit
|
|
140
140
|
validator; report shipped only after the exact `.project/`-only ship commit
|
|
141
141
|
passes the postcommit validator and `validate-integrated` proves the
|
|
142
|
-
|
|
142
|
+
integration landing, its tag, and its ancestry on origin/main —
|
|
143
143
|
while integration is pending the router routes back to ship instead of
|
|
144
144
|
reporting shipped or starting the next milestone.
|
|
145
145
|
|
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,50 @@ app and the daemon have their own file,
|
|
|
11
11
|
[daemon/app/CHANGELOG.md](daemon/app/CHANGELOG.md). Release bookkeeping and
|
|
12
12
|
test-only changes are not listed; the Git history has them.
|
|
13
13
|
|
|
14
|
+
## [1.6.2] - 2026-10-17
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- record 1.6.2 live host release evidence
|
|
18
|
+
- prepare 1.6.2 patch candidate
|
|
19
|
+
- Document README What's new heading in RELEASE.md
|
|
20
|
+
- update release notes for v1.6.1
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- keep the README What's new section current from release notes
|
|
24
|
+
- read quoted STATE values in the ship checkpoint guard
|
|
25
|
+
- refuse a build checkpoint while STATE is ship/active
|
|
26
|
+
- keep evidence-line citations when final review reuses a wave
|
|
27
|
+
## [1.6.1] - 2026-10-16
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- accept GitHub rebase merges in pull-request integration
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- record 1.6.1 live host release evidence
|
|
34
|
+
- prepare 1.6.1 patch candidate
|
|
35
|
+
- Note PR rebase-merge landing in CONTEXT, WORKFLOW, AGENTS
|
|
36
|
+
- Note owner-authorized push exception in FULL.md and HOOKS.md
|
|
37
|
+
- correct the minimum Git version in TEST_ENVIRONMENT.md
|
|
38
|
+
- prove the landed member base through the file scope check
|
|
39
|
+
- describe project Update with no project install
|
|
40
|
+
- update release notes for v1.6.0
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- run dispatch_driver subprocess helpers from the selected runtime
|
|
44
|
+
- refuse a relative model_policy --record and check release before locking
|
|
45
|
+
- let promote-next replace a DOCS-AUDIT carry-forward the lookahead keeps
|
|
46
|
+
- let the owner authorize one exact pre-ship push to a task branch
|
|
47
|
+
- post Discord changelogs from automated releases
|
|
48
|
+
- stage a landing or checkpoint change set with one git add
|
|
49
|
+
- pass the landing message and member paths to git through stdin
|
|
50
|
+
- do not check the paths that a landed task brief names
|
|
51
|
+
- accept a landed task as the only owner of an Interface contract bullet
|
|
52
|
+
- say why a project has no workflow status in the desktop app
|
|
53
|
+
|
|
54
|
+
### Other
|
|
55
|
+
- Accept only same value or T### for pending rulings
|
|
56
|
+
- list not-started projects and re-probe after project install
|
|
57
|
+
- sort idle projects with shipped in legacy dashboard
|
|
14
58
|
## [1.6.0] - 2026-10-12
|
|
15
59
|
|
|
16
60
|
### Added
|
package/FULL.md
CHANGED
|
@@ -322,9 +322,9 @@ approve **Archive and ship**, the archive transaction begins:
|
|
|
322
322
|
- `MANIFEST.md` records contents and ship metadata
|
|
323
323
|
- One **ship commit** (subject `ship: M00N — <slug>`) touches only `.project/`
|
|
324
324
|
- Ship then either merges `gsd-path/M00N` onto `main` directly or opens a
|
|
325
|
-
GitHub PR and waits for you to merge it with a merge commit
|
|
326
|
-
include a GSD Path credit footer.
|
|
327
|
-
- After the
|
|
325
|
+
GitHub PR and waits for you to merge it with a merge commit or a rebase
|
|
326
|
+
merge. Path-created PRs include a GSD Path credit footer.
|
|
327
|
+
- After the landing validates, Path tags `milestone/<NNN>-<slug>`
|
|
328
328
|
- Archives are **read-only** — guard hooks enforce this if installed
|
|
329
329
|
|
|
330
330
|
Direct integration is the project default. Before build, ask the router to set
|
|
@@ -454,7 +454,8 @@ approves.
|
|
|
454
454
|
- You cannot share a milestone in progress through the remote. With
|
|
455
455
|
[guard hooks](HOOKS.md) installed, the pre-push hook refuses `gsd-path/M00N`
|
|
456
456
|
before its ship commit. It also refuses any other branch that carries the
|
|
457
|
-
unfinished state
|
|
457
|
+
unfinished state, unless the owner authorizes that exact commit (see
|
|
458
|
+
[HOOKS.md](HOOKS.md)). Hooks are optional and local to each clone. A clone
|
|
458
459
|
without hooks has no such check.
|
|
459
460
|
|
|
460
461
|
**Do not**
|
package/HOOKS.md
CHANGED
|
@@ -351,6 +351,22 @@ such as `rm -rf scratch` passes the archive check; other guard rules still apply
|
|
|
351
351
|
without `STATE.md` passes on an ordinary ref. An absent bound ref passes
|
|
352
352
|
deletion. The authorized publisher is `archive_milestone.py integrate`;
|
|
353
353
|
the hook checks ref updates, not which client initiated them
|
|
354
|
+
- **allows** an owner-approved push of unshipped work, such as a task
|
|
355
|
+
branch that CI must measure before the task can finish. The owner records
|
|
356
|
+
the exact commit for the exact branch, then pushes it:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
git update-ref refs/gsd-path/authorizations/<project>/push/refs/heads/<branch> <commit>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`<project>` is the `project` field in that commit's `STATE.md`. The hook
|
|
363
|
+
accepts the push only at that commit; a later commit, such as one that
|
|
364
|
+
removes the measurement inputs, needs its own record. Bound `gsd-path/M###`
|
|
365
|
+
refs, the default branch, and tags never accept an authorization. The
|
|
366
|
+
authorization accepts every push of that commit to that branch until the
|
|
367
|
+
owner removes it with `git update-ref -d <same ref>`. Deleting the remote
|
|
368
|
+
branch afterwards needs no authorization. Only the owner records an
|
|
369
|
+
authorization; an agent asks the owner for one
|
|
354
370
|
- **allows** adding files to archive during the ship transaction, before the
|
|
355
371
|
branch closes
|
|
356
372
|
|
|
@@ -358,8 +374,8 @@ These local hooks cover pushes made by plain Git and clients that invoke
|
|
|
358
374
|
Git with hooks enabled. Creating a PR from an already published ref does not
|
|
359
375
|
run the pre-push hook. A branch cut from `main` carrying `shipped/done` passes;
|
|
360
376
|
if `main` instead carries an unshipped bound state, pushes of branches carrying
|
|
361
|
-
that state remain blocked
|
|
362
|
-
pipeline.
|
|
377
|
+
that state remain blocked, except owner-authorized commits, until the
|
|
378
|
+
milestone records are repaired through the pipeline.
|
|
363
379
|
|
|
364
380
|
## GitHub-side gate
|
|
365
381
|
|
package/QUICK.md
CHANGED
|
@@ -14,7 +14,7 @@ One router skill drives a gated pipeline: inspect → define → research → de
|
|
|
14
14
|
plan → build → ship. Every handoff lives in `.project/` so any session can resume from disk.
|
|
15
15
|
|
|
16
16
|
**Hosts:** Codex, Claude Code, Grok, OpenCode, Antigravity, Cursor, and Kimi
|
|
17
|
-
Code have a passing live milestone for release 1.6.
|
|
17
|
+
Code have a passing live milestone for release 1.6.2. Copilot CLI, Qwen, Zed,
|
|
18
18
|
Kiro, and Muse Code install, but have no live milestone for that release. See
|
|
19
19
|
[host tiers](README.md).
|
|
20
20
|
|
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@ milestone loop. While a milestone builds, the next one can be planned in
|
|
|
9
9
|
parallel under `.project/next/` (lookahead); a building milestone can also be
|
|
10
10
|
abandoned on an explicit ruling, archiving its partial work for a re-slice.
|
|
11
11
|
|
|
12
|
-
**Hosts with a passing full live milestone for release 1.6.
|
|
12
|
+
**Hosts with a passing full live milestone for release 1.6.2:** Codex, Claude
|
|
13
13
|
Code, Grok, OpenCode, Antigravity CLI, Cursor, and Kimi Code.
|
|
14
14
|
|
|
15
15
|
**Hosts with an installer and a dispatch contract, but no live milestone for
|
|
16
|
-
release 1.6.
|
|
16
|
+
release 1.6.2:** GitHub Copilot CLI (account quota exhausted), Qwen Code, Zed,
|
|
17
17
|
and Kiro (API credits), and Muse Code (no live release evidence recorded yet).
|
|
18
18
|
These hosts are excluded from the release evaluation. Exclusion is not a pass.
|
|
19
19
|
|
|
@@ -24,14 +24,16 @@ The [host trust matrix](https://github.com/open-gsd/gsd-path/blob/main/docs/trus
|
|
|
24
24
|
is the source of truth for live milestone proof and each host's guard tier.
|
|
25
25
|
|
|
26
26
|
<!-- release-docs -->
|
|
27
|
-
|
|
27
|
+
## What's new in 1.6.2 — 2026-10-17
|
|
28
|
+
|
|
29
|
+
**Latest npm release:** [@opengsd/gsd-path@1.6.2](https://www.npmjs.com/package/@opengsd/gsd-path/v/1.6.2) — [release notes](CHANGELOG.md#162---2026-10-17)
|
|
28
30
|
|
|
29
31
|
**Recent highlights**
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
32
|
+
- keep the README What's new section current from release notes
|
|
33
|
+
- read quoted STATE values in the ship checkpoint guard
|
|
34
|
+
- refuse a build checkpoint while STATE is ship/active
|
|
35
|
+
- keep evidence-line citations when final review reuses a wave
|
|
36
|
+
- record 1.6.2 live host release evidence
|
|
35
37
|
|
|
36
38
|
<!-- /release-docs -->
|
|
37
39
|
|
|
@@ -55,25 +57,6 @@ is the source of truth for live milestone proof and each host's guard tier.
|
|
|
55
57
|
| [RELEASE.md](RELEASE.md) | Maintainer CI and npm release cycle |
|
|
56
58
|
| [GUIDE.md](GUIDE.md) | Pointer to the guides above |
|
|
57
59
|
|
|
58
|
-
## What's new — October 2, 2026
|
|
59
|
-
|
|
60
|
-
These changes are merged into the source checkout; npm installs use the latest
|
|
61
|
-
published release.
|
|
62
|
-
|
|
63
|
-
- **Desktop app (pre-release):** one app for macOS, Windows, and Linux shows
|
|
64
|
-
your projects, manages skills in each coding agent, and updates itself. See
|
|
65
|
-
[Desktop app](#desktop-app).
|
|
66
|
-
- **Skills from the published release:** the monitor installs skills from the
|
|
67
|
-
npm release, checks the download against the registry's sha512, and refuses
|
|
68
|
-
one that does not match. A release picker lets you stay on an older version.
|
|
69
|
-
- **Safer monitor writes:** a monitor started by the app accepts changes only
|
|
70
|
-
with the app's local token, so another web page cannot change your setup.
|
|
71
|
-
- **Environment editor:** edit a project's `.env` files with values masked and
|
|
72
|
-
a diff before each save.
|
|
73
|
-
- **Usage charts:** tokens and cost per day, time per phase, tasks per wave,
|
|
74
|
-
and verify history. Missing data shows as missing, never as zero.
|
|
75
|
-
- **Muse Code** is a supported host.
|
|
76
|
-
|
|
77
60
|
## Install from npm
|
|
78
61
|
|
|
79
62
|
Requires **Node.js 18.17+** for the npm installer and **Python 3.9+** for
|
|
@@ -465,7 +448,8 @@ an atomic close across repos.
|
|
|
465
448
|
Shipping moves milestone artifacts into `archive/` with a MANIFEST. It then
|
|
466
449
|
either merges the milestone's `gsd-path/M00N` branch into `main` directly or
|
|
467
450
|
opens/reuses a GitHub PR and waits for its user-controlled merge. Path reports
|
|
468
|
-
shipped only after a two-parent merge
|
|
451
|
+
shipped only after the landing (a two-parent merge, or a proven GitHub rebase
|
|
452
|
+
merge of the PR) and the milestone tag validate. Before
|
|
469
453
|
any next-milestone files change, the router binds
|
|
470
454
|
a new `gsd-path/M00N` at the updated `origin/main`. `STATE.md`, `REPOSITORY.md`,
|
|
471
455
|
`LESSONS.md`, `next/`, and the program artifacts (`CHARTER.md`, `ROADMAP.md`,
|
package/WORKFLOW.md
CHANGED
|
@@ -513,7 +513,11 @@ untouched.
|
|
|
513
513
|
One exception: a DOCS-AUDIT.md with pending `planned: no` rulings is copied
|
|
514
514
|
atomically back into a recreated `research/` so the alignment queue survives;
|
|
515
515
|
the archived original keeps full history. Active and archived research may
|
|
516
|
-
coexist only for that byte-identical carry-forward.
|
|
516
|
+
coexist only for that byte-identical carry-forward. When a lookahead track
|
|
517
|
+
brings its own `research/`, `promote-next` removes the carry-forward in the
|
|
518
|
+
promotion commit, but only if the lookahead DOCS-AUDIT.md keeps every
|
|
519
|
+
carry-forward ruling (a pending row may gain a `T###`); otherwise promotion
|
|
520
|
+
stops.
|
|
517
521
|
|
|
518
522
|
Archives become read-only when committed. No phase may modify a committed
|
|
519
523
|
archive; the next milestone's inspection may read it. A retry with a persisted
|
|
@@ -612,9 +616,9 @@ and requires the remote default to be the recorded
|
|
|
612
616
|
- `pull-request` requires `gh` authentication for GitHub.com and a GitHub.com
|
|
613
617
|
origin. Path publishes the exact ship commit and creates or reuses the one
|
|
614
618
|
eligible PR to `main` with a GSD Path credit footer. It returns
|
|
615
|
-
`awaiting-merge` until a human GitHub user merges it with a merge commit
|
|
616
|
-
never enables auto-merge or merges the PR. After
|
|
617
|
-
annotated milestone tag. The [ship contract](skills/gsd-path-ship/SKILL.md)
|
|
619
|
+
`awaiting-merge` until a human GitHub user merges it with a merge commit or a
|
|
620
|
+
rebase merge; Path never enables auto-merge or merges the PR. After
|
|
621
|
+
validation, Path creates the annotated milestone tag. The [ship contract](skills/gsd-path-ship/SKILL.md)
|
|
618
622
|
owns the candidate, provenance, topology, publication, and recovery rules.
|
|
619
623
|
- `external-landing` is for an origin that is not GitHub.com, such as GitLab,
|
|
620
624
|
Gitea, or GitHub Enterprise Server, where a merge request is the only route
|
package/package.json
CHANGED
package/scripts/_common.py
CHANGED
|
@@ -361,6 +361,49 @@ def run_git(
|
|
|
361
361
|
return _run_exact(git_command(repo, *arguments), input=input)
|
|
362
362
|
|
|
363
363
|
|
|
364
|
+
def _patch_ids(repo: Path, commits: Sequence[str]) -> list[Optional[str]]:
|
|
365
|
+
"""`git patch-id --stable` per commit, in order; None for an empty commit."""
|
|
366
|
+
log = run_git(repo, "log", "--no-walk=unsorted", "-p", "--binary", "--no-color",
|
|
367
|
+
"--no-ext-diff", *commits)
|
|
368
|
+
if log.returncode != 0:
|
|
369
|
+
raise ValueError("could not read commit patches")
|
|
370
|
+
ids = run_git(repo, "patch-id", "--stable", input=log.stdout)
|
|
371
|
+
if ids.returncode != 0:
|
|
372
|
+
raise ValueError("could not compute patch ids")
|
|
373
|
+
by_commit = {commit: patch for patch, commit in (line.split() for line in ids.stdout.splitlines())}
|
|
374
|
+
return [by_commit.get(commit) for commit in commits]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def rebase_landing_problem(repo: Path, ship: str, landing: str) -> Optional[str]:
|
|
378
|
+
"""Why landing is not a forge rebase merge of ship, or None when it is (#418).
|
|
379
|
+
|
|
380
|
+
A rebase merge replays ship's own commits onto the default branch. Proof:
|
|
381
|
+
the first-parent run that ends at landing has one non-merge commit per
|
|
382
|
+
non-merge commit of landing..ship, with equal patch ids in the same order;
|
|
383
|
+
when that run starts at ship's fork point, landing's tree equals ship's.
|
|
384
|
+
"""
|
|
385
|
+
listed = run_git(repo, "rev-list", "--reverse", "--topo-order", "--no-merges", f"{landing}..{ship}")
|
|
386
|
+
if listed.returncode != 0:
|
|
387
|
+
return "could not list the ship commits"
|
|
388
|
+
originals = listed.stdout.split()
|
|
389
|
+
if not originals:
|
|
390
|
+
return "landing already contains the ship commit"
|
|
391
|
+
walked = run_git(repo, "rev-list", "--first-parent", "--parents", "-n", str(len(originals)), landing)
|
|
392
|
+
rows = [line.split() for line in walked.stdout.splitlines()][::-1]
|
|
393
|
+
if walked.returncode != 0 or len(rows) != len(originals) or any(len(row) != 2 for row in rows):
|
|
394
|
+
return "landing is not a linear run of rebased commits"
|
|
395
|
+
try:
|
|
396
|
+
if _patch_ids(repo, originals) != _patch_ids(repo, [row[0] for row in rows]):
|
|
397
|
+
return "rebased commits do not replay the ship commits in order"
|
|
398
|
+
except ValueError as error:
|
|
399
|
+
return str(error)
|
|
400
|
+
fork = run_git(repo, "merge-base", ship, landing).stdout.strip()
|
|
401
|
+
trees = run_git(repo, "rev-parse", f"{ship}^{{tree}}", f"{landing}^{{tree}}").stdout.split()
|
|
402
|
+
if rows[0][1] == fork and (len(trees) != 2 or trees[0] != trees[1]):
|
|
403
|
+
return "rebased tree differs from the ship tree"
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
|
|
364
407
|
def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
|
|
365
408
|
"""Git argv for repo, including WSL drvfs workarounds when needed."""
|
|
366
409
|
return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
|
|
@@ -387,7 +387,9 @@ def _lint_task(
|
|
|
387
387
|
)
|
|
388
388
|
break
|
|
389
389
|
|
|
390
|
-
|
|
390
|
+
# A landed brief is history that nobody can edit: only an unlanded brief must name present paths.
|
|
391
|
+
landed = task_id in (landed_bases or {})
|
|
392
|
+
for name in () if landed else PROSE_SECTIONS:
|
|
391
393
|
body = sections.get(name)
|
|
392
394
|
if body is None or not _clean(body):
|
|
393
395
|
continue
|
|
@@ -400,8 +402,6 @@ def _lint_task(
|
|
|
400
402
|
and token not in supplied
|
|
401
403
|
and not _supplied_contract(repo, path, token)
|
|
402
404
|
and not _pipeline_workspace_file(repo, path, token)
|
|
403
|
-
# A landed brief is history: the pipeline moves its own files (build recovery, archive) later.
|
|
404
|
-
and not (task_id in (landed_bases or {}) and _pipeline_workspace_path(repo, path, token))
|
|
405
405
|
and not _base_exists(git_root, base, token)
|
|
406
406
|
):
|
|
407
407
|
problems.append(f"## {name} names a path missing at the layer base: {token}")
|
|
@@ -411,7 +411,7 @@ def _lint_task(
|
|
|
411
411
|
block = VERIFY_BLOCK_PATTERN.search(verify_body)
|
|
412
412
|
if block is None or not block.group("block").strip():
|
|
413
413
|
problems.append("## Verify must contain a non-empty fenced bash block")
|
|
414
|
-
|
|
414
|
+
elif not landed:
|
|
415
415
|
for token in _verify_tokens(
|
|
416
416
|
block.group("block"),
|
|
417
417
|
repo=repo,
|
|
@@ -448,9 +448,10 @@ def validate_task_briefs(
|
|
|
448
448
|
|
|
449
449
|
Plain lint uses the layer base without overrides. Plan approval and dispatch
|
|
450
450
|
supply transitive dependency files for unlanded tasks and resolved historical
|
|
451
|
-
bases for landed tasks
|
|
451
|
+
bases for landed tasks. A landed brief is immutable: its `files` scope is
|
|
452
|
+
checked at that base, and the paths its prose and Verify name are not checked.
|
|
452
453
|
The caller validates the task graph and landed metadata before supplying
|
|
453
|
-
these mappings
|
|
454
|
+
these mappings.
|
|
454
455
|
"""
|
|
455
456
|
|
|
456
457
|
resolved_base = _resolve_base(root, base)
|
|
@@ -481,6 +482,9 @@ def validate_task_briefs(
|
|
|
481
482
|
for shape, task_ids in contracts.items():
|
|
482
483
|
if len(task_ids) == 1:
|
|
483
484
|
task_id = next(iter(task_ids))
|
|
485
|
+
# A landed brief is history: a later repair may reword the bullet in its partner.
|
|
486
|
+
if task_id in (landed_bases or {}):
|
|
487
|
+
continue
|
|
484
488
|
problems.append(
|
|
485
489
|
f"{task_id}: interface contract is not shared by any other task: {shape}"
|
|
486
490
|
)
|
|
@@ -592,7 +596,6 @@ def plan_validation_bases(
|
|
|
592
596
|
)
|
|
593
597
|
except IsolationError as error:
|
|
594
598
|
raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
|
|
595
|
-
dependency_files[task_id] = set()
|
|
596
599
|
except check_handoffs.HandoffError as error:
|
|
597
600
|
raise BriefError(str(error)) from error
|
|
598
601
|
return tasks, dependency_files, landed_bases
|
|
@@ -117,9 +117,21 @@ def _refresh_stop_errors() -> None:
|
|
|
117
117
|
model_policy.PolicyError)
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
# The selected Project runtime directory once _reload_pinned_modules ran; None keeps bundled helpers.
|
|
121
|
+
_RUNTIME_ROOT: Optional[Path] = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def helper_script(name: str) -> Path:
|
|
125
|
+
"""A subprocess helper from the selected runtime when it ships one, else the bundled sibling."""
|
|
126
|
+
if _RUNTIME_ROOT is not None and (_RUNTIME_ROOT / name).is_file():
|
|
127
|
+
return _RUNTIME_ROOT / name
|
|
128
|
+
return Path(__file__).resolve().parent / name
|
|
129
|
+
|
|
130
|
+
|
|
120
131
|
def _reload_pinned_modules(runtime_root: Path) -> None:
|
|
121
132
|
global isolation, build_state, task_context, contracts, pipeline_state
|
|
122
|
-
global archive_milestone, review_findings, model_policy
|
|
133
|
+
global archive_milestone, review_findings, model_policy, _RUNTIME_ROOT
|
|
134
|
+
_RUNTIME_ROOT = runtime_root
|
|
123
135
|
if _bundled_helpers():
|
|
124
136
|
path = str(runtime_root)
|
|
125
137
|
if path not in sys.path:
|
|
@@ -981,7 +993,7 @@ class Round:
|
|
|
981
993
|
diagnose = shlex.join([
|
|
982
994
|
sys.executable,
|
|
983
995
|
"-B",
|
|
984
|
-
str(
|
|
996
|
+
str(helper_script("pipeline_diagnose.py")),
|
|
985
997
|
"diagnose",
|
|
986
998
|
"--repo",
|
|
987
999
|
str(self.primary),
|
|
@@ -1677,8 +1689,8 @@ class Review:
|
|
|
1677
1689
|
|
|
1678
1690
|
|
|
1679
1691
|
def helper_json(receipt: Dict[str, object], script: str, *arguments: str, cwd: Path) -> Dict[str, object]:
|
|
1680
|
-
"""Run a bundled
|
|
1681
|
-
command = [sys.executable, "-B", str(
|
|
1692
|
+
"""Run a helper (runtime-selected, else bundled) and return its JSON stdout; a non-zero exit is a typed stop."""
|
|
1693
|
+
command = [sys.executable, "-B", str(helper_script(script)), *arguments]
|
|
1682
1694
|
completed = subprocess.run(command, cwd=cwd, capture_output=True, encoding="utf-8", errors="replace")
|
|
1683
1695
|
step = {"script": script, "command": command, "exit_code": completed.returncode,
|
|
1684
1696
|
"stdout": completed.stdout, "stderr": completed.stderr}
|
|
@@ -2290,7 +2302,7 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
2290
2302
|
if plan_changed:
|
|
2291
2303
|
_common.atomic_write(plan_path, updated)
|
|
2292
2304
|
receipt["plan_wave"] = fix_wave
|
|
2293
|
-
lint = subprocess.run([sys.executable, "-B", str(
|
|
2305
|
+
lint = subprocess.run([sys.executable, "-B", str(helper_script("check_task_briefs.py")),
|
|
2294
2306
|
"--repo", str(primary), "--base", isolation.current_sha(primary),
|
|
2295
2307
|
"--tasks-dir", f"{options.project_dir}/tasks",
|
|
2296
2308
|
"--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
|
package/scripts/git_guard.py
CHANGED
|
@@ -69,6 +69,11 @@ PUBLICATION_HINT = (
|
|
|
69
69
|
"(STATE shipped/done) through archive_milestone.py integrate; findings during "
|
|
70
70
|
"ship reopen through the patch plan (see SHIP.md)"
|
|
71
71
|
)
|
|
72
|
+
AUTHORIZED_PUSH_HINT = (
|
|
73
|
+
"an owner-approved push of one exact commit to a branch other than the bound "
|
|
74
|
+
"or default branch needs refs/gsd-path/authorizations/<project>/push/<ref> "
|
|
75
|
+
"at that commit (see HOOKS.md)"
|
|
76
|
+
)
|
|
72
77
|
LEGACY_STATE_FIELDS = {
|
|
73
78
|
"pipeline",
|
|
74
79
|
"project",
|
|
@@ -621,14 +626,25 @@ def pre_push_violations(lines):
|
|
|
621
626
|
continue
|
|
622
627
|
state = state_at(local_sha)
|
|
623
628
|
owed = unshipped_bound_branch(state) if state is not None else None
|
|
624
|
-
if owed is not None:
|
|
629
|
+
if owed is not None and not authorized_publication(state, remote_ref, local_sha):
|
|
625
630
|
found.append(
|
|
626
631
|
f"{remote_ref} <- {local_sha[:12]} carries unshipped {owed} work "
|
|
627
|
-
f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}"
|
|
632
|
+
f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}; "
|
|
633
|
+
f"{AUTHORIZED_PUSH_HINT}"
|
|
628
634
|
)
|
|
629
635
|
return found
|
|
630
636
|
|
|
631
637
|
|
|
638
|
+
def authorized_publication(state, remote_ref, sha):
|
|
639
|
+
"""The owner authorized this exact commit on this one non-default branch."""
|
|
640
|
+
project = state.get("project")
|
|
641
|
+
if not isinstance(project, str) or not members.NAME_RE.fullmatch(project):
|
|
642
|
+
return False
|
|
643
|
+
if not remote_ref.startswith("refs/heads/") or remote_ref == f"refs/heads/{default_branch()}":
|
|
644
|
+
return False
|
|
645
|
+
return members.authorized(repo_root(), project, "push", remote_ref, sha)
|
|
646
|
+
|
|
647
|
+
|
|
632
648
|
def is_ship_commit(subject):
|
|
633
649
|
return bool(subject and subject.strip().casefold().startswith("ship:"))
|
|
634
650
|
|
package/scripts/integration.py
CHANGED
|
@@ -845,6 +845,8 @@ def require_pull_request_merge(
|
|
|
845
845
|
merge_commit: str,
|
|
846
846
|
ship_commit: str,
|
|
847
847
|
remote_default: str,
|
|
848
|
+
*,
|
|
849
|
+
allow_rebase: bool = False,
|
|
848
850
|
) -> None:
|
|
849
851
|
resolved = optional_ref(project, merge_commit)
|
|
850
852
|
if resolved != merge_commit:
|
|
@@ -853,9 +855,16 @@ def require_pull_request_merge(
|
|
|
853
855
|
run_git(project, "rev-list", "--parents", "-n", "1", merge_commit),
|
|
854
856
|
"inspect pull-request merge parents",
|
|
855
857
|
).split()
|
|
856
|
-
if len(parents)
|
|
858
|
+
if allow_rebase and len(parents) == 2:
|
|
859
|
+
problem = _common.rebase_landing_problem(project, ship_commit, merge_commit)
|
|
860
|
+
if problem is not None:
|
|
861
|
+
raise ArchiveError(
|
|
862
|
+
"pull-request integration must use a two-parent merge commit "
|
|
863
|
+
f"or a rebase merge of the ship commits: {problem}"
|
|
864
|
+
)
|
|
865
|
+
elif len(parents) != 3:
|
|
857
866
|
raise ArchiveError("pull-request integration must use a two-parent merge commit")
|
|
858
|
-
|
|
867
|
+
elif parents[2] != ship_commit:
|
|
859
868
|
raise ArchiveError("pull-request merge second parent is not the ship commit")
|
|
860
869
|
first_parent = archive_milestone.require_git_success(
|
|
861
870
|
run_git(project, "rev-list", "--first-parent", remote_default),
|
|
@@ -1026,7 +1035,9 @@ def integrate_pull_request(
|
|
|
1026
1035
|
merge_commit,
|
|
1027
1036
|
)
|
|
1028
1037
|
remote_default = refresh_origin(project)["remote_default"]
|
|
1029
|
-
require_pull_request_merge(
|
|
1038
|
+
require_pull_request_merge(
|
|
1039
|
+
project, merge_commit, ship_commit, remote_default, allow_rebase=True
|
|
1040
|
+
)
|
|
1030
1041
|
pull = update_pull_request_body(
|
|
1031
1042
|
repository,
|
|
1032
1043
|
pull,
|
|
@@ -1473,7 +1484,9 @@ def validate_integrated(
|
|
|
1473
1484
|
pull["number"],
|
|
1474
1485
|
merge_commit,
|
|
1475
1486
|
)
|
|
1476
|
-
require_pull_request_merge(
|
|
1487
|
+
require_pull_request_merge(
|
|
1488
|
+
project, merge_commit, ship_commit, remote_default, allow_rebase=True
|
|
1489
|
+
)
|
|
1477
1490
|
require_annotated_tag(
|
|
1478
1491
|
project,
|
|
1479
1492
|
tag_ref,
|
package/scripts/isolation.py
CHANGED
|
@@ -1219,9 +1219,17 @@ def _commit_member_source(sidecar: Path, journal: Dict[str, object], allowed: Se
|
|
|
1219
1219
|
undeclared = undeclared_paths(sidecar, member_base, pending, allowed)
|
|
1220
1220
|
if undeclared:
|
|
1221
1221
|
raise IsolationError("undeclared paths: " + ", ".join(undeclared))
|
|
1222
|
-
git_output(sidecar, "add", "-A", "--", *sorted(pending))
|
|
1223
1222
|
body = member_commit_body(str(journal["task_file"]), sorted(pending), member_base, str(journal["base"]))
|
|
1224
|
-
|
|
1223
|
+
# The paths and the body grow with the change set, so both go through
|
|
1224
|
+
# stdin: a Windows command line holds about 32 KiB.
|
|
1225
|
+
for arguments, text in (
|
|
1226
|
+
(("add", "-A", "--pathspec-from-file=-", "--pathspec-file-nul"),
|
|
1227
|
+
"".join(f"{path}\0" for path in sorted(pending))),
|
|
1228
|
+
(("commit", "-q", "-F", "-"), _commit_message(str(journal["subject"]), body)),
|
|
1229
|
+
):
|
|
1230
|
+
result = run_git(sidecar, *arguments, input=text)
|
|
1231
|
+
if result.returncode != 0:
|
|
1232
|
+
raise IsolationError((result.stderr or result.stdout).strip() or "git command failed")
|
|
1225
1233
|
source = current_sha(sidecar)
|
|
1226
1234
|
if source == member_base:
|
|
1227
1235
|
raise IsolationError("no changes to land")
|
|
@@ -1669,6 +1677,25 @@ def committed_paths_since(repo: Path, base: str) -> Set[str]:
|
|
|
1669
1677
|
)
|
|
1670
1678
|
|
|
1671
1679
|
|
|
1680
|
+
def _stage_paths(repo: Path, mode: str, paths: Set[str]) -> None:
|
|
1681
|
+
"""Stage `paths` with one `git add`.
|
|
1682
|
+
|
|
1683
|
+
One call for each path fails when a file replaces a tracked directory: the
|
|
1684
|
+
first call drops the files below the directory from the index, and then
|
|
1685
|
+
their pathspecs match no file. The `./` prefix stops pathspec magic, so a
|
|
1686
|
+
name such as `:!b` cannot exclude another path of the set. Git names the
|
|
1687
|
+
path in its error text.
|
|
1688
|
+
"""
|
|
1689
|
+
if not paths:
|
|
1690
|
+
return
|
|
1691
|
+
added = run_git(
|
|
1692
|
+
repo, "add", mode, "--pathspec-from-file=-", "--pathspec-file-nul",
|
|
1693
|
+
input="".join(f"./{path}\0" for path in sorted(paths)),
|
|
1694
|
+
)
|
|
1695
|
+
if added.returncode != 0:
|
|
1696
|
+
raise IsolationError(f"could not stage the change set: {added.stderr.strip()}")
|
|
1697
|
+
|
|
1698
|
+
|
|
1672
1699
|
def ignored_legacy_ledger(repo: Path) -> bool:
|
|
1673
1700
|
"""True when a verify ledger absent from HEAD is hidden by an ignore rule.
|
|
1674
1701
|
|
|
@@ -1684,17 +1711,25 @@ def ignored_legacy_ledger(repo: Path) -> bool:
|
|
|
1684
1711
|
return run_git(repo, "check-ignore", "-q", "--no-index", "--", VERIFY_LEDGER_PATH).returncode == 0
|
|
1685
1712
|
|
|
1686
1713
|
|
|
1714
|
+
def _commit_message(subject: str, body: str) -> str:
|
|
1715
|
+
"""The message for `git commit -F -`.
|
|
1716
|
+
|
|
1717
|
+
A landing body lists every changed path. As a `-m` argument it can exceed
|
|
1718
|
+
the Windows command-line limit, so the message goes through stdin. Like
|
|
1719
|
+
`-m`, the message ends its last line.
|
|
1720
|
+
"""
|
|
1721
|
+
message = f"{subject}\n\n{body}"
|
|
1722
|
+
return message if message.endswith("\n") else message + "\n"
|
|
1723
|
+
|
|
1724
|
+
|
|
1687
1725
|
def _commit_pending(
|
|
1688
1726
|
repo: Path, pending: Set[str], subject: str, body: str, forced: Set[str] = frozenset()
|
|
1689
1727
|
) -> str:
|
|
1690
1728
|
reset = run_git(repo, "reset", "-q", "HEAD")
|
|
1691
1729
|
if reset.returncode != 0:
|
|
1692
1730
|
raise IsolationError("could not clear the index before commit")
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
added = run_git(repo, "add", mode, "--", path)
|
|
1696
|
-
if added.returncode != 0:
|
|
1697
|
-
raise IsolationError(f"could not stage {path}: {added.stderr.strip()}")
|
|
1731
|
+
_stage_paths(repo, "-A", pending - forced)
|
|
1732
|
+
_stage_paths(repo, "-f", pending & forced)
|
|
1698
1733
|
staged = _split_paths(
|
|
1699
1734
|
git_output(
|
|
1700
1735
|
repo,
|
|
@@ -1707,7 +1742,7 @@ def _commit_pending(
|
|
|
1707
1742
|
)
|
|
1708
1743
|
if staged != pending:
|
|
1709
1744
|
raise IsolationError("staged paths do not match the uncommitted change set")
|
|
1710
|
-
committed = run_git(repo, "commit", "-q", "-
|
|
1745
|
+
committed = run_git(repo, "commit", "-q", "-F", "-", input=_commit_message(subject, body))
|
|
1711
1746
|
if committed.returncode != 0:
|
|
1712
1747
|
raise IsolationError((committed.stderr or committed.stdout).strip() or "commit failed")
|
|
1713
1748
|
return current_sha(repo)
|
|
@@ -1786,18 +1821,15 @@ def commit_allowed_changes(
|
|
|
1786
1821
|
reset = run_git(repo, "reset", "-q", "HEAD")
|
|
1787
1822
|
if reset.returncode != 0:
|
|
1788
1823
|
raise IsolationError("could not clear the index before landing")
|
|
1789
|
-
|
|
1790
|
-
added = run_git(repo, "add", "-A", "--", path)
|
|
1791
|
-
if added.returncode != 0:
|
|
1792
|
-
raise IsolationError(f"could not stage {path}: {added.stderr.strip()}")
|
|
1824
|
+
_stage_paths(repo, "-A", staging)
|
|
1793
1825
|
staged = _split_paths(
|
|
1794
|
-
git_output(repo, "diff", "--cached", "--name-only", "--relative")
|
|
1826
|
+
git_output(repo, "diff", "--no-renames", "--cached", "--name-only", "--relative")
|
|
1795
1827
|
)
|
|
1796
1828
|
if staged != staging:
|
|
1797
1829
|
raise IsolationError("staged paths do not match the uncommitted change set")
|
|
1798
1830
|
verified_tree = git_output(repo, "write-tree")
|
|
1799
1831
|
verified_delta = _tree_delta(repo, base, verified_tree)
|
|
1800
|
-
committed = run_git(repo, "commit", "-q", "-
|
|
1832
|
+
committed = run_git(repo, "commit", "-q", "-F", "-", input=_commit_message(subject, body))
|
|
1801
1833
|
if committed.returncode != 0:
|
|
1802
1834
|
raise IsolationError(
|
|
1803
1835
|
(committed.stderr or committed.stdout).strip() or "commit failed"
|
|
@@ -4470,6 +4502,20 @@ def attest(
|
|
|
4470
4502
|
}
|
|
4471
4503
|
|
|
4472
4504
|
|
|
4505
|
+
def _committed_ship_active(repo: Path) -> bool:
|
|
4506
|
+
"""True when the STATE.md committed at HEAD is ship/active."""
|
|
4507
|
+
|
|
4508
|
+
result = run_git(repo, "show", "HEAD:.project/STATE.md")
|
|
4509
|
+
if result.returncode != 0:
|
|
4510
|
+
return False
|
|
4511
|
+
frontmatter = result.stdout.split("\n---", 1)[0]
|
|
4512
|
+
fields = {
|
|
4513
|
+
key: _unquote(value)
|
|
4514
|
+
for key, value in re.findall(r"(?m)^(phase|status):(.*)$", frontmatter)
|
|
4515
|
+
}
|
|
4516
|
+
return fields.get("phase") == "ship" and fields.get("status") == "active"
|
|
4517
|
+
|
|
4518
|
+
|
|
4473
4519
|
def checkpoint(
|
|
4474
4520
|
repo: Path,
|
|
4475
4521
|
expected_head: str,
|
|
@@ -4519,6 +4565,12 @@ def checkpoint(
|
|
|
4519
4565
|
"subject": subject.strip(),
|
|
4520
4566
|
}
|
|
4521
4567
|
raise IsolationError("primary HEAD differs from --expected-head")
|
|
4568
|
+
if subject.strip().startswith("build:") and _committed_ship_active(repo):
|
|
4569
|
+
raise IsolationError(
|
|
4570
|
+
"STATE is ship/active: final review creates no pre-ship commit; leave its "
|
|
4571
|
+
"outputs uncommitted for archive_milestone.py, and send findings through "
|
|
4572
|
+
"the patch plan (see SHIP.md)"
|
|
4573
|
+
)
|
|
4522
4574
|
pending = uncommitted_paths(repo)
|
|
4523
4575
|
forced: Set[str] = set()
|
|
4524
4576
|
# Build only: an approval undo resets to a parent without the ledger and would drop it.
|
|
@@ -108,10 +108,14 @@ def _final_view(relative, text, criteria, reviewed, head, surfaces=None):
|
|
|
108
108
|
fields[field] = contracts._raw_source_field(block, field, relative)
|
|
109
109
|
if "Surface" in fields and not {"Check", "Observed"}.issubset(fields):
|
|
110
110
|
raise contracts.HandoffError(f"{criterion} lacks its recorded surface walkthrough")
|
|
111
|
+
reference = f"{relative} — {criterion}"
|
|
112
|
+
if "Observed" in fields and evidence:
|
|
113
|
+
# The reviewer may cite held-out and judgment ids on the evidence line.
|
|
114
|
+
reference += f"; evidence: {evidence}"
|
|
111
115
|
lines += [f"### {criterion} — {title}", "", "- **Verdict**: met",
|
|
112
116
|
f"- **Check**: {fields.get('Check', 'none')}",
|
|
113
117
|
f"- **Observed**: {fields.get('Observed', evidence)}",
|
|
114
|
-
f"- **Reference**: {
|
|
118
|
+
f"- **Reference**: {reference}",
|
|
115
119
|
"- **Finding**: none", "- **Fix direction**: none"]
|
|
116
120
|
if "Surface" in fields:
|
|
117
121
|
expected = (surfaces or {}).get(criterion)
|