triad-plus 1.10.0 → 1.12.0
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 +19 -0
- package/README.md +41 -0
- package/bin/triad-plus.js +565 -20
- package/docs/configuration.md +31 -0
- package/docs/human-readable-reports.md +52 -0
- package/docs/npx-installation.md +61 -4
- package/docs/operating-guide.it.md +9 -0
- package/docs/operating-guide.md +9 -0
- package/docs/troubleshooting.md +41 -0
- package/package.json +2 -2
- package/runtime/lib/evidence.mjs +11 -0
- package/runtime/lib/fingerprint.mjs +63 -0
- package/runtime/lib/human-reports.mjs +513 -0
- package/runtime/lib/installation-manifest.mjs +269 -0
- package/runtime/triad-human-report.mjs +50 -0
- package/skills/triad-loop-bootstrap/SKILL.md +4 -1
- package/skills/triad-loop-bootstrap/assets/loop-template/card-report.template.md +32 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/handoff-report.template.md +14 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/runtime/report-context/card.template.json +28 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/runtime/report-context/delivery.template.json +16 -0
- package/skills/triad-loop-orchestrator/SKILL.md +48 -0
package/docs/configuration.md
CHANGED
|
@@ -31,6 +31,37 @@ adapter writes those into host-native profiles only where the selected host
|
|
|
31
31
|
supports that facility. A blank model means the host default. Never put tokens,
|
|
32
32
|
API keys, or private deployment data in this file.
|
|
33
33
|
|
|
34
|
+
## Installation ownership and version
|
|
35
|
+
|
|
36
|
+
The package version and the installed workspace version are separate facts:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx triad-plus --version
|
|
40
|
+
npx triad-plus version --control /path/to/project-control
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After successful materialization, `.triad-plus/installation.json` records the
|
|
44
|
+
adapter, project/global scopes, exact managed files, SHA-256 hashes, timestamps,
|
|
45
|
+
and a deterministic fingerprint. Project paths are control-workspace-relative;
|
|
46
|
+
global paths identify the user-level managed asset. The manifest is generated
|
|
47
|
+
from the same adapter registry and install plan used by `init` and `upgrade`.
|
|
48
|
+
|
|
49
|
+
The manifest deliberately does not own `.triad-plus/team.json`, `.loop/`,
|
|
50
|
+
`project.yaml`, feature cards, artifacts, evidence, or product source. A failed
|
|
51
|
+
install never writes a success manifest. Legacy workspaces are migrated by
|
|
52
|
+
`upgrade --apply` using the currently executing package version; the previous
|
|
53
|
+
version is not guessed.
|
|
54
|
+
|
|
55
|
+
When a team configuration is materialized, the managed role-run block in
|
|
56
|
+
`AGENTS.md` is recorded as a bounded block asset. Uninstall removes that block
|
|
57
|
+
only when its markers and hash still match, never the surrounding user file.
|
|
58
|
+
Global assets are shared-capable and therefore preserved by default by
|
|
59
|
+
`uninstall --global`.
|
|
60
|
+
|
|
61
|
+
`doctor` reports CLI version, installed version, manifest state, adapter, and
|
|
62
|
+
project/global scope. It reports version skew explicitly and never queries npm
|
|
63
|
+
for `latest`.
|
|
64
|
+
|
|
34
65
|
## Configure models through the agent
|
|
35
66
|
|
|
36
67
|
The installed `triad-model-configuration` skill lets an owner ask the selected
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Human-readable reports
|
|
2
|
+
|
|
3
|
+
Triad+ keeps JSON/YAML control records, verification evidence, review reports,
|
|
4
|
+
candidate fingerprints, and delivery records as the source of truth. From those
|
|
5
|
+
records the Orchestrator can materialize two portable Markdown views:
|
|
6
|
+
|
|
7
|
+
- one `card-reports/<card-id>.md` for every terminal Card;
|
|
8
|
+
- one final delivery handoff with a human-first summary and links to the Card
|
|
9
|
+
reports.
|
|
10
|
+
|
|
11
|
+
The report renderer is deterministic and does not make an additional LLM call.
|
|
12
|
+
It attributes changed paths to the complete Card baseline, so a rework attempt
|
|
13
|
+
does not hide files changed by an earlier attempt. Reports contain relative
|
|
14
|
+
evidence references and redact unrelated absolute paths.
|
|
15
|
+
|
|
16
|
+
## Materialize a Card report
|
|
17
|
+
|
|
18
|
+
The Orchestrator first writes a small derived JSON context from the canonical
|
|
19
|
+
Card, attempt, verifier, Reviewer, and delivery records. The source Card and
|
|
20
|
+
those records remain read-only. Then run:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node .triad-runtime/triad-human-report.mjs \
|
|
24
|
+
--mode card \
|
|
25
|
+
--project /absolute/path/to/control-workspace \
|
|
26
|
+
--input .loop/runtime/report-context/CARD-001.json \
|
|
27
|
+
--output card-reports/CARD-001.md \
|
|
28
|
+
--worktree /absolute/path/to/product-worktree \
|
|
29
|
+
--base-commit <card-baseline-commit>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For an approved Card the renderer requires a final commit, candidate
|
|
33
|
+
fingerprint, passing verifier evidence, independent Reviewer approval, and the
|
|
34
|
+
complete changed-path manifest. A blocked or not-delivered terminal Card must
|
|
35
|
+
include a truthful reason and never claims delivery. Writes are atomic and
|
|
36
|
+
re-running the command with the same context is idempotent.
|
|
37
|
+
|
|
38
|
+
## Materialize the final handoff
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node .triad-runtime/triad-human-report.mjs \
|
|
42
|
+
--mode handoff \
|
|
43
|
+
--project /absolute/path/to/control-workspace \
|
|
44
|
+
--input .loop/runtime/report-context/delivery.json \
|
|
45
|
+
--output handoff.md
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The handoff opens with the executive summary and Card results, then preserves
|
|
49
|
+
links to the technical evidence, branch/commit map, quality-contract closure,
|
|
50
|
+
Evaluator+ result, delivery gates, demo details, risks, and practical test.
|
|
51
|
+
It is a view for people, not an alternative delivery state machine.
|
|
52
|
+
|
package/docs/npx-installation.md
CHANGED
|
@@ -22,11 +22,68 @@ npx triad-plus init --host codex --control /path/to/project-control --global
|
|
|
22
22
|
npx triad-plus doctor --host codex --control /path/to/project-control
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Version visibility
|
|
26
26
|
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
`--version` reports the package that is actually executing:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx triad-plus --version
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The workspace command reports the materialized installation recorded by the
|
|
34
|
+
control workspace manifest:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx triad-plus version --control /path/to/project-control
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
An older workspace without `.triad-plus/installation.json` is reported as a
|
|
41
|
+
legacy installation. Triad+ does not infer a historical version from scattered
|
|
42
|
+
agent files.
|
|
43
|
+
|
|
44
|
+
## Installation manifest and safe uninstall
|
|
45
|
+
|
|
46
|
+
After a successful `init`, Triad+ writes
|
|
47
|
+
`.triad-plus/installation.json`. It records the selected adapter, CLI version,
|
|
48
|
+
project/global scopes, normalized managed-file paths, SHA-256 hashes, and a
|
|
49
|
+
manifest fingerprint. `upgrade --apply` refreshes this record after managed
|
|
50
|
+
assets are materialized; a legacy workspace receives a new manifest without
|
|
51
|
+
inventing its previous version.
|
|
52
|
+
|
|
53
|
+
Uninstall is dry-run by default:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx triad-plus uninstall --host opencode --control /path/to/project-control
|
|
57
|
+
npx triad-plus uninstall --host opencode --control /path/to/project-control --apply
|
|
58
|
+
npx triad-plus uninstall --host opencode --control /path/to/project-control --global --apply
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Only files listed in the manifest, still unchanged from their recorded hash,
|
|
62
|
+
are removed. Missing files are reported as `ABSENT`; modified assets are
|
|
63
|
+
preserved. Host directories are never pruned because directory ownership is
|
|
64
|
+
not claimed. The team configuration, `.loop/`, project manifest, feature
|
|
65
|
+
cards, artifacts, evidence, and other user state are preserved. A managed
|
|
66
|
+
`AGENTS.md` role-run block is tracked separately and removed only when its
|
|
67
|
+
markers and hash are exact; surrounding user content remains. A manifest
|
|
68
|
+
remains as an `uninstalled` or `partial` tombstone so a second uninstall is
|
|
69
|
+
idempotent and the ownership history is auditable.
|
|
70
|
+
|
|
71
|
+
`--global --apply` is deliberately preserve-by-default: global assets may be
|
|
72
|
+
shared by several control workspaces, so the command reports them as shared
|
|
73
|
+
and does not delete them without a cross-workspace ownership model.
|
|
74
|
+
|
|
75
|
+
Read-only commands require an existing control workspace; a typo path is
|
|
76
|
+
never created by `version` or `uninstall`.
|
|
77
|
+
|
|
78
|
+
## Upgrade, repair, or restore an existing control workspace
|
|
79
|
+
|
|
80
|
+
`init` is the first-install command and refuses to overwrite an existing
|
|
81
|
+
workspace. `upgrade --apply` is the managed update, repair, and restore path
|
|
82
|
+
for a workspace that is already registered by an installation manifest,
|
|
83
|
+
including a workspace whose project scope is `uninstalled` or `partial` after a
|
|
84
|
+
safe uninstall. It re-materializes project assets, reuses the preserved
|
|
85
|
+
`team.json`, and refreshes the manifest without changing user state, `.loop/`,
|
|
86
|
+
PRD files, evidence, or product repositories. The default is a dry run:
|
|
30
87
|
|
|
31
88
|
```bash
|
|
32
89
|
npx triad-plus upgrade --host codex --control /path/to/project-control --global
|
|
@@ -96,6 +96,15 @@ inserire token o segreti. Gli hook sono un’ottimizzazione, non autorità: il
|
|
|
96
96
|
dispatch esplicito della verification resta sempre disponibile. Vedi la
|
|
97
97
|
[matrice di compatibilità](compatibility.md).
|
|
98
98
|
|
|
99
|
+
Per ogni Card terminale l’Orchestrator può generare una vista deterministica
|
|
100
|
+
`card-reports/<card-id>.md` dai record canonici di Card, attempt, verifier e
|
|
101
|
+
Reviewer. Il renderer attribuisce al baseline completo della Card il delta dei
|
|
102
|
+
path modificati, inclusi rework, rinomina e cancellazione. L’handoff finale si
|
|
103
|
+
apre con un riepilogo leggibile e collega questi report, mantenendo anche le
|
|
104
|
+
evidence tecniche di delivery. I report sono scritti atomicamente, sono
|
|
105
|
+
ripetibili e usano riferimenti relativi: non sono una nuova source of truth né
|
|
106
|
+
una chiamata aggiuntiva a un agente. Vedi la [guida ai report leggibili](human-readable-reports.md).
|
|
107
|
+
|
|
99
108
|
Per ogni demo configurata, registra nel progetto e nell’handoff comando, URL
|
|
100
109
|
locale, modalità di accesso remoto e URL remoto. `localhost` è solo locale: non
|
|
101
110
|
va indicato a chi prova da remoto. Avvia il servizio soltanto su richiesta del
|
package/docs/operating-guide.md
CHANGED
|
@@ -95,6 +95,15 @@ reports, and handoff in a separate project-control workspace. Never place tokens
|
|
|
95
95
|
or secrets there. Hooks are an optimization, not authority; explicit verification
|
|
96
96
|
is always the fallback. See the [compatibility matrix](compatibility.md).
|
|
97
97
|
|
|
98
|
+
For every terminal Card, the Orchestrator may render a deterministic
|
|
99
|
+
`card-reports/<card-id>.md` view from the canonical Card, attempt, verifier, and
|
|
100
|
+
Reviewer records. The renderer attributes the complete changed-path delta to the
|
|
101
|
+
Card baseline, including rework, renames, and deletions. The final handoff opens
|
|
102
|
+
with a human-readable summary and links to those reports while retaining the
|
|
103
|
+
technical delivery evidence. Reports are atomic, repeatable, relative-path
|
|
104
|
+
views; they are not a new source of truth or an additional agent call. See the
|
|
105
|
+
[human-readable reports guide](human-readable-reports.md).
|
|
106
|
+
|
|
98
107
|
For a configured demo service, record its command, local URL, remote-access mode,
|
|
99
108
|
and remote URL in the project and handoff. `localhost` is local-only; do not give
|
|
100
109
|
it to a remote tester as a reachable endpoint. Start the service only on the
|
package/docs/troubleshooting.md
CHANGED
|
@@ -6,6 +6,26 @@ Run doctor first:
|
|
|
6
6
|
npx triad-plus doctor --host <runtime> --control /path/to/triad-control
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
Compare the executing CLI with the materialized workspace installation:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx triad-plus --version
|
|
13
|
+
npx triad-plus version --control /path/to/triad-control
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`legacy / manifest missing` means the workspace predates the installation
|
|
17
|
+
manifest. Run `upgrade --apply` to materialize a current manifest; Triad+ does
|
|
18
|
+
not infer the old version. The same `upgrade --apply` command is the managed
|
|
19
|
+
restore path after a safe uninstall leaves an `uninstalled` or `partial`
|
|
20
|
+
manifest: it re-materializes managed assets and reuses the preserved team
|
|
21
|
+
configuration and user state. `init` is reserved for first installation and
|
|
22
|
+
refuses existing paths. `manifest invalid` means the ownership record or its
|
|
23
|
+
fingerprint is malformed and should be reviewed before any uninstall.
|
|
24
|
+
|
|
25
|
+
Doctor also reports `CLI newer / upgrade available` and `CLI older than
|
|
26
|
+
installed version` instead of silently claiming compatibility. It never checks
|
|
27
|
+
the npm `latest` tag.
|
|
28
|
+
|
|
9
29
|
`not installed` means the selected adapter assets are absent from that control
|
|
10
30
|
workspace. `not installed or version unavailable` for a host means its binary is
|
|
11
31
|
not on PATH or otherwise cannot answer `--version`. Re-run the appropriate host
|
|
@@ -20,3 +40,24 @@ baselines, worktree, branch, and candidate. Do not treat it as a passing test.
|
|
|
20
40
|
|
|
21
41
|
If Evaluator+ is unavailable, check `roles.evaluator.enabled` in the team file.
|
|
22
42
|
An Evaluator+ failure is post-run information, not an automatic repair request.
|
|
43
|
+
|
|
44
|
+
## Safe uninstall
|
|
45
|
+
|
|
46
|
+
Uninstall is a dry run unless `--apply` is supplied:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx triad-plus uninstall --host <runtime> --control /path/to/triad-control
|
|
50
|
+
npx triad-plus uninstall --host <runtime> --control /path/to/triad-control --apply
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Only unchanged files listed in `.triad-plus/installation.json` are removed.
|
|
54
|
+
`ABSENT` files are harmless. A `PRESERVE` line means the file was modified, no
|
|
55
|
+
longer matches the trusted managed plan, or is not a regular file; no force
|
|
56
|
+
option exists for this operation. Add `--global` only when user-level Triad
|
|
57
|
+
assets should also be considered. Global assets are nevertheless preserved by
|
|
58
|
+
default because another workspace may share them. Team config, loop state,
|
|
59
|
+
cards, artifacts, evidence, generic host directories, and other user files are
|
|
60
|
+
preserved. A managed `AGENTS.md` block is removed only when its exact markers
|
|
61
|
+
and hash still match; a modified or ambiguous block leaves the uninstall
|
|
62
|
+
partial. `version` and `uninstall` reject nonexistent control paths without
|
|
63
|
+
creating them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triad-plus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "A lightweight, evidence-backed engineering loop for coding agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=20"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"test": "node tests/runtime-forward-test.mjs && node tests/retry-scope-contracts-test.mjs && node tests/cli-install-test.mjs && node tests/codex-liveness-test.mjs && node tests/bmad-story-importer-test.mjs && node tests/bmad-epics-intake-test.mjs && node tests/immutable-quality-contract-test.mjs && node tests/assignment-packet-test.mjs && node tests/repository-context-test.mjs && node tests/tui-model-configuration-test.mjs",
|
|
31
|
+
"test": "node tests/runtime-forward-test.mjs && node tests/retry-scope-contracts-test.mjs && node tests/cli-install-test.mjs && node tests/installation-manifest-test.mjs && node tests/codex-liveness-test.mjs && node tests/bmad-story-importer-test.mjs && node tests/bmad-epics-intake-test.mjs && node tests/immutable-quality-contract-test.mjs && node tests/assignment-packet-test.mjs && node tests/repository-context-test.mjs && node tests/tui-model-configuration-test.mjs && node tests/human-reports-test.mjs",
|
|
32
32
|
"pack:check": "npm pack --dry-run"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
package/runtime/lib/evidence.mjs
CHANGED
|
@@ -29,3 +29,14 @@ export async function writeAtomicJson(targetPath, value) {
|
|
|
29
29
|
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
30
30
|
await rename(temporaryPath, targetPath);
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Persist a derived human-readable artifact without exposing a partially
|
|
35
|
+
* written report to the owner or to a resumed Orchestrator.
|
|
36
|
+
*/
|
|
37
|
+
export async function writeAtomicText(targetPath, value) {
|
|
38
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
39
|
+
const temporaryPath = `${targetPath}.tmp`;
|
|
40
|
+
await writeFile(temporaryPath, String(value), "utf8");
|
|
41
|
+
await rename(temporaryPath, targetPath);
|
|
42
|
+
}
|
|
@@ -26,6 +26,12 @@ async function gitOutput(worktree, args) {
|
|
|
26
26
|
return result.stdout;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
async function verifiedCommit(root, commit, label) {
|
|
30
|
+
const resolved = (await gitLines(root, ["rev-parse", "--verify", `${commit}^{commit}`]))[0];
|
|
31
|
+
if (!resolved) throw new Error(`${label} does not resolve to a commit`);
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
function ignored(relativePath) {
|
|
30
36
|
return SENSITIVE_PATH.test(relativePath) || IGNORED_PATH.test(relativePath);
|
|
31
37
|
}
|
|
@@ -84,6 +90,63 @@ export async function collectCandidateChanges(worktree, { baseCommit = null } =
|
|
|
84
90
|
return { git_head: head, base_commit: base, changes, ignored_paths: [...new Set(ignored_paths)].sort() };
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Collect a complete candidate delta from an immutable Git commit rather than
|
|
95
|
+
* from whatever happens to be in the current worktree. This is used by derived
|
|
96
|
+
* reports so a later Card on the same branch cannot contaminate an earlier one.
|
|
97
|
+
*/
|
|
98
|
+
export async function collectCandidateChangesAtCommit(worktree, { baseCommit, commit }) {
|
|
99
|
+
const root = await realpath(worktree);
|
|
100
|
+
const base = await verifiedCommit(root, baseCommit, "base commit");
|
|
101
|
+
const head = await verifiedCommit(root, commit, "final commit");
|
|
102
|
+
const ancestry = await runProcess("git", ["merge-base", "--is-ancestor", base, head], { cwd: root, timeoutMs: 15_000 });
|
|
103
|
+
if (ancestry.exitCode !== 0) throw new Error("card baseline is not an ancestor of the final commit");
|
|
104
|
+
const tracked = parseNameStatus(await gitOutput(root, ["diff", "--name-status", "--find-renames", base, head]));
|
|
105
|
+
const changes = [];
|
|
106
|
+
const ignored_paths = [];
|
|
107
|
+
for (const entry of tracked) {
|
|
108
|
+
const entryPaths = pathsFor(entry);
|
|
109
|
+
if (entryPaths.some(ignored)) {
|
|
110
|
+
ignored_paths.push(...entryPaths.filter(ignored));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (const relativePath of entryPaths) {
|
|
114
|
+
const absolutePath = path.resolve(root, relativePath);
|
|
115
|
+
if (!absolutePath.startsWith(`${root}${path.sep}`)) throw new Error(`unsafe changed path: ${relativePath}`);
|
|
116
|
+
}
|
|
117
|
+
changes.push(entry);
|
|
118
|
+
}
|
|
119
|
+
return { git_head: head, base_commit: base, changes, ignored_paths: [...new Set(ignored_paths)].sort() };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function commitFileHash(root, commit, relativePath) {
|
|
123
|
+
const result = await runProcess("git", ["show", `${commit}:${relativePath}`], { cwd: root, timeoutMs: 15_000 });
|
|
124
|
+
if (result.exitCode !== 0) throw new Error(`git show failed for ${commit}:${relativePath}`);
|
|
125
|
+
return digest(result.stdout);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Calculate the same candidate fingerprint algorithm at a specific commit. */
|
|
129
|
+
export async function calculateCandidateFingerprintAtCommit(worktree, { baseCommit, commit }) {
|
|
130
|
+
const root = await realpath(worktree);
|
|
131
|
+
const candidate = await collectCandidateChangesAtCommit(root, { baseCommit, commit });
|
|
132
|
+
const files = [];
|
|
133
|
+
const changed = new Map();
|
|
134
|
+
for (const entry of candidate.changes) {
|
|
135
|
+
if (entry.status === "renamed") {
|
|
136
|
+
changed.set(entry.source, "DELETED");
|
|
137
|
+
changed.set(entry.destination, null);
|
|
138
|
+
} else {
|
|
139
|
+
changed.set(entry.path, entry.status === "deleted" ? "DELETED" : null);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [relativePath, knownHash] of [...changed.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
143
|
+
const contentHash = knownHash === null ? await commitFileHash(root, candidate.git_head, relativePath) : knownHash;
|
|
144
|
+
files.push({ path: relativePath, sha256: contentHash });
|
|
145
|
+
}
|
|
146
|
+
const canonical = JSON.stringify({ git_head: candidate.git_head, files });
|
|
147
|
+
return { algorithm: "sha256", value: digest(canonical), git_head: candidate.git_head, files };
|
|
148
|
+
}
|
|
149
|
+
|
|
87
150
|
export async function worktreeBranch(worktree) {
|
|
88
151
|
const root = await realpath(worktree);
|
|
89
152
|
return (await gitLines(root, ["branch", "--show-current"]))[0] ?? "DETACHED";
|