@deftai/directive-core 0.108.0 → 0.109.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/dist/cache/operations.js +1 -1
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -1
- package/dist/consumer-check-contract/evaluate.d.ts +47 -0
- package/dist/consumer-check-contract/evaluate.js +181 -15
- package/dist/delivery-attempt/evaluate.d.ts +9 -1
- package/dist/delivery-attempt/evaluate.js +69 -0
- package/dist/delivery-attempt/index.d.ts +1 -1
- package/dist/delivery-attempt/index.js +1 -1
- package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
- package/dist/deposit/live-procedure-exclusions.js +110 -0
- package/dist/deposit/live-procedure-targets.d.ts +45 -0
- package/dist/deposit/live-procedure-targets.js +274 -0
- package/dist/deposit/python-free.d.ts +6 -0
- package/dist/deposit/python-free.js +15 -0
- package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
- package/dist/deposit/rewrite-deposit-links.js +148 -0
- package/dist/deposit/run-stage-content-pack.d.ts +2 -0
- package/dist/deposit/run-stage-content-pack.js +3 -0
- package/dist/deposit/stage-content-pack.d.ts +17 -0
- package/dist/deposit/stage-content-pack.js +91 -0
- package/dist/design-critique/citation-grammar.d.ts +7 -0
- package/dist/design-critique/citation-grammar.js +1 -1
- package/dist/design-critique/completed-arc-record.d.ts +1 -1
- package/dist/design-critique/completed-arc-record.js +59 -14
- package/dist/hooks/classify/host-session-identity.d.ts +24 -11
- package/dist/hooks/classify/host-session-identity.js +46 -49
- package/dist/hooks/classify/index.d.ts +2 -2
- package/dist/hooks/classify/index.js +2 -2
- package/dist/hooks/classify/paths.d.ts +2 -0
- package/dist/hooks/classify/paths.js +8 -4
- package/dist/hooks/classify/stdin.js +69 -3
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +6 -2
- package/dist/hooks/dispatcher.js +258 -62
- package/dist/hooks/fixtures/cases.d.ts +6 -0
- package/dist/hooks/fixtures/cases.js +53 -0
- package/dist/hooks/git-destructive-log.d.ts +32 -0
- package/dist/hooks/git-destructive-log.js +46 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/shell-write-targets.d.ts +10 -0
- package/dist/hooks/shell-write-targets.js +274 -0
- package/dist/hooks/tools.d.ts +20 -2
- package/dist/hooks/tools.js +30 -1
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/init-deposit.js +3 -0
- package/dist/init-deposit/refresh.js +7 -0
- package/dist/init-deposit/runtime-writers.d.ts +14 -0
- package/dist/init-deposit/runtime-writers.js +33 -0
- package/dist/intake/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +21 -2
- package/dist/intake/reconcile-issues.js +13 -13
- package/dist/lifecycle/brief-envelope.d.ts +25 -0
- package/dist/lifecycle/brief-envelope.js +42 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/evaluate.js +14 -5
- package/dist/literal-acceptance/index.d.ts +1 -1
- package/dist/literal-acceptance/index.js +1 -1
- package/dist/literal-acceptance/run.d.ts +2 -0
- package/dist/literal-acceptance/run.js +19 -1
- package/dist/orphan-active/candidate-scope.d.ts +53 -0
- package/dist/orphan-active/candidate-scope.js +157 -0
- package/dist/orphan-active/evaluate.d.ts +27 -0
- package/dist/orphan-active/evaluate.js +63 -6
- package/dist/orphan-active/index.d.ts +1 -0
- package/dist/orphan-active/index.js +1 -0
- package/dist/policy/merge-approval-head.js +7 -6
- package/dist/pr-closing-keywords/gh.js +32 -9
- package/dist/pr-closing-keywords/main.d.ts +1 -0
- package/dist/pr-closing-keywords/main.js +68 -3
- package/dist/pr-closing-keywords/types.d.ts +2 -0
- package/dist/pr-merge-readiness/gh.js +32 -9
- package/dist/pr-protected-issues/gh.js +36 -9
- package/dist/pr-wait-mergeable/wrappers.js +3 -3
- package/dist/product-first-done-gate/evaluate.js +6 -2
- package/dist/product-first-done-gate/types.js +2 -0
- package/dist/release/consumer-hard-stops.d.ts +48 -0
- package/dist/release/consumer-hard-stops.js +140 -0
- package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
- package/dist/release/consumer-readiness-disclosure.js +51 -0
- package/dist/release/index.d.ts +2 -0
- package/dist/release/index.js +2 -0
- package/dist/release/pipeline.js +55 -0
- package/dist/release/run-consumer-readiness.d.ts +15 -0
- package/dist/release/run-consumer-readiness.js +24 -0
- package/dist/release/types.d.ts +6 -0
- package/dist/review-monitor/github-lease.js +4 -3
- package/dist/run-summary/types.d.ts +2 -2
- package/dist/scm/build-command.d.ts +2 -2
- package/dist/scm/build-command.js +2 -2
- package/dist/scm/call-shape.d.ts +25 -0
- package/dist/scm/call-shape.js +59 -0
- package/dist/scm/call.d.ts +7 -4
- package/dist/scm/call.js +41 -11
- package/dist/scm/gh-rest.d.ts +25 -12
- package/dist/scm/gh-rest.js +65 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- package/dist/scm/spawn-status.d.ts +33 -0
- package/dist/scm/spawn-status.js +53 -0
- package/dist/scope/acceptance-evidence.d.ts +1 -1
- package/dist/scope/transition.js +4 -14
- package/dist/session/ac-pass-banking.d.ts +2 -2
- package/dist/session/ac-pass-banking.js +2 -2
- package/dist/session/host-session-owner.d.ts +53 -0
- package/dist/session/host-session-owner.js +89 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +21 -1
- package/dist/session/occupancy.js +70 -12
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/triage/evaluate/worktrees.js +153 -6
- package/dist/umbrella-current-shape/index.d.ts +51 -3
- package/dist/umbrella-current-shape/index.js +106 -18
- package/dist/validate-content/deposit-required.d.ts +39 -0
- package/dist/validate-content/deposit-required.js +147 -0
- package/dist/validate-content/index.d.ts +1 -0
- package/dist/validate-content/index.js +1 -0
- package/dist/validate-content/validate-links.d.ts +2 -3
- package/dist/validate-content/validate-links.js +29 -2
- package/dist/vbrief-activate/activate.d.ts +7 -2
- package/dist/vbrief-activate/activate.js +26 -13
- package/dist/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-source/deposit-closure.d.ts +23 -0
- package/dist/verify-source/deposit-closure.js +162 -0
- package/dist/verify-source/index.d.ts +2 -0
- package/dist/verify-source/index.js +2 -0
- package/dist/verify-source/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +3 -3
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage the @deftai/directive-content pack the same way prepack does, then
|
|
3
|
+
* rewrite relative markdown links onto the flattened deposit layout (#3937).
|
|
4
|
+
*
|
|
5
|
+
* Must not write into the source tree: destDir is the pack root (packages/content
|
|
6
|
+
* during prepack, or an out-of-tree temp dir in tests).
|
|
7
|
+
*/
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, } from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
11
|
+
import { rewriteMarkdownDepositLinks, sourceRelForPackRel } from "./rewrite-deposit-links.js";
|
|
12
|
+
const ENGINE_ENTRIES = [".githooks", "Taskfile.yml", "tasks"];
|
|
13
|
+
const HARNESS_ENTRIES = ["main.md", "SKILL.md"];
|
|
14
|
+
function keepCopy(src) {
|
|
15
|
+
return !src.includes("__pycache__") && !src.endsWith(".pyc") && !src.endsWith(".py");
|
|
16
|
+
}
|
|
17
|
+
function rewriteStagedMarkdown(destDir) {
|
|
18
|
+
let filesRewritten = 0;
|
|
19
|
+
const walk = (dir) => {
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const full = join(dir, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
walk(full);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
34
|
+
continue;
|
|
35
|
+
const packRel = relative(destDir, full).replace(/\\/g, "/");
|
|
36
|
+
const original = readFileSync(full, "utf8");
|
|
37
|
+
const { content, rewriteCount } = rewriteMarkdownDepositLinks({
|
|
38
|
+
content: original,
|
|
39
|
+
sourceFileRel: sourceRelForPackRel(packRel),
|
|
40
|
+
packFileRel: packRel,
|
|
41
|
+
});
|
|
42
|
+
if (rewriteCount > 0 && content !== original) {
|
|
43
|
+
containedWrite({
|
|
44
|
+
root: destDir,
|
|
45
|
+
target: packRel,
|
|
46
|
+
data: content,
|
|
47
|
+
mode: "replace",
|
|
48
|
+
});
|
|
49
|
+
filesRewritten += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
walk(destDir);
|
|
54
|
+
return filesRewritten;
|
|
55
|
+
}
|
|
56
|
+
/** Copy content/ + harness + engine entries, then rewrite markdown links. */
|
|
57
|
+
export function stageContentPack(options) {
|
|
58
|
+
const { repoRoot, destDir } = options;
|
|
59
|
+
mkdirSync(destDir, { recursive: true });
|
|
60
|
+
const contentSrc = join(repoRoot, "content");
|
|
61
|
+
for (const name of readdirSync(contentSrc)) {
|
|
62
|
+
const from = join(contentSrc, name);
|
|
63
|
+
const to = join(destDir, name);
|
|
64
|
+
if (existsSync(to))
|
|
65
|
+
rmSync(to, { recursive: true, force: true });
|
|
66
|
+
cpSync(from, to, { recursive: true, filter: (src) => keepCopy(src) });
|
|
67
|
+
}
|
|
68
|
+
for (const name of ENGINE_ENTRIES) {
|
|
69
|
+
const from = join(repoRoot, name);
|
|
70
|
+
if (!existsSync(from))
|
|
71
|
+
continue;
|
|
72
|
+
const to = join(destDir, name);
|
|
73
|
+
if (existsSync(to))
|
|
74
|
+
rmSync(to, { recursive: true, force: true });
|
|
75
|
+
cpSync(from, to, { recursive: true, filter: (src) => keepCopy(src) });
|
|
76
|
+
}
|
|
77
|
+
for (const name of HARNESS_ENTRIES) {
|
|
78
|
+
const from = join(repoRoot, name);
|
|
79
|
+
if (!existsSync(from))
|
|
80
|
+
continue;
|
|
81
|
+
cpSync(from, join(destDir, name));
|
|
82
|
+
}
|
|
83
|
+
const filesRewritten = rewriteStagedMarkdown(destDir);
|
|
84
|
+
return { filesRewritten };
|
|
85
|
+
}
|
|
86
|
+
/** prepack cwd is packages/content. */
|
|
87
|
+
export function runPrepackFromContentPackage(pkgDir) {
|
|
88
|
+
const repoRoot = join(pkgDir, "..", "..");
|
|
89
|
+
stageContentPack({ repoRoot, destDir: pkgDir });
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=stage-content-pack.js.map
|
|
@@ -44,6 +44,13 @@ export type CitationScan = {
|
|
|
44
44
|
};
|
|
45
45
|
/** The published closed set, in the order the contract lists it. */
|
|
46
46
|
export declare const ACCEPTED_CITATION_FORMS: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Refused positions are exactly the five published classes. An indented code
|
|
49
|
+
* block and an HTML comment are deliberately not classified: a four-space
|
|
50
|
+
* indent is also ordinary list-continuation content, so refusing it would block
|
|
51
|
+
* valid records more often than it would catch example text.
|
|
52
|
+
*/
|
|
53
|
+
export declare function classifyPosition(body: string, offset: number): CitationRejectionClass | null;
|
|
47
54
|
/**
|
|
48
55
|
* Parse the closed citation grammar out of one comment body. Refused positions
|
|
49
56
|
* are reported rather than dropped so a block detail can echo the observation
|
|
@@ -239,7 +239,7 @@ function blockPosition(body, offset) {
|
|
|
239
239
|
* indent is also ordinary list-continuation content, so refusing it would block
|
|
240
240
|
* valid records more often than it would catch example text.
|
|
241
241
|
*/
|
|
242
|
-
function classifyPosition(body, offset) {
|
|
242
|
+
export function classifyPosition(body, offset) {
|
|
243
243
|
const block = blockPosition(body, offset);
|
|
244
244
|
if (block.insideFence)
|
|
245
245
|
return "code-fence";
|
|
@@ -15,7 +15,7 @@ export type ThreadComment = {
|
|
|
15
15
|
readonly id: number;
|
|
16
16
|
readonly body: string;
|
|
17
17
|
};
|
|
18
|
-
export type CompletedArcBlockReason = "missing-record" | "lone-shape" | "cite-not-lean" | "missing-table-cite";
|
|
18
|
+
export type CompletedArcBlockReason = "missing-record" | "lone-shape" | "cite-not-lean" | "missing-table-cite" | "ambiguous-table-cite";
|
|
19
19
|
export type CompletedArcVerdict = {
|
|
20
20
|
readonly status: "not-in-arc";
|
|
21
21
|
} | {
|
|
@@ -126,6 +126,61 @@ function resolveCitedLean(citations, byCommentId, latestLean) {
|
|
|
126
126
|
.map((row) => byCommentId.get(row.id))
|
|
127
127
|
.find((row) => row !== undefined && isSuccessorLeanBody(row.body));
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Table resolution precedence (#3932).
|
|
131
|
+
*
|
|
132
|
+
* A typed `verified-claims table <id>` citation is the synthesis naming its own
|
|
133
|
+
* table, so it decides resolution: a generic citation neither satisfies it nor
|
|
134
|
+
* masks it. Scanning every citation for the first table-shaped body -- what
|
|
135
|
+
* this did before -- let an unrelated table-shaped comment clear a record whose
|
|
136
|
+
* claimed table is not a table, because the `missing-table-cite` refusal ran
|
|
137
|
+
* only after that untyped search failed.
|
|
138
|
+
*
|
|
139
|
+
* Cardinality rule: every table-kind claim must resolve to the same
|
|
140
|
+
* verified-claims table artifact. One valid typed claim must not launder an
|
|
141
|
+
* invalid one -- filtering to typed citations and then taking the first
|
|
142
|
+
* table-shaped body finds the real table in a valid-plus-ghost pair and never
|
|
143
|
+
* examines the ghost. `scanCitations` deduplicates on `(id, kind)`, so naming
|
|
144
|
+
* one table id twice is a single claim, while two distinct typed ids are two
|
|
145
|
+
* claims and cannot both be this record's table.
|
|
146
|
+
*
|
|
147
|
+
* With no typed claim the generic scan stands unchanged: permalink and bare
|
|
148
|
+
* `comment` citations scan as kind `comment`, and that is the published form
|
|
149
|
+
* these records use. Narrowing the citation contract so a table must be named
|
|
150
|
+
* by keyword is a separate decision needing its own migration criteria.
|
|
151
|
+
*/
|
|
152
|
+
function resolveCitedTable(citations, byCommentId) {
|
|
153
|
+
const claimed = citations.filter((row) => row.kind === "table");
|
|
154
|
+
if (claimed.length === 0) {
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
table: citations
|
|
158
|
+
.map((row) => byCommentId.get(row.id))
|
|
159
|
+
.find((row) => row !== undefined && isVerifiedClaimsTableBody(row.body)),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const resolved = claimed.map((row) => ({ id: row.id, cited: byCommentId.get(row.id) }));
|
|
163
|
+
const unresolved = resolved.filter((row) => row.cited === undefined || !isVerifiedClaimsTableBody(row.cited.body));
|
|
164
|
+
if (unresolved.length > 0) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
reason: "missing-table-cite",
|
|
168
|
+
detail: "synthesis cites a verified-claims table id that is not a table on this thread: " +
|
|
169
|
+
renderIds(unresolved.map((row) => row.id)),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const distinct = [...new Set(claimed.map((row) => row.id))];
|
|
173
|
+
if (distinct.length > 1) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
reason: "ambiguous-table-cite",
|
|
177
|
+
detail: "synthesis claims more than one verified-claims table; every table citation must name " +
|
|
178
|
+
"the same table: " +
|
|
179
|
+
renderIds(distinct),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, table: resolved.find((row) => row.cited !== undefined)?.cited };
|
|
183
|
+
}
|
|
129
184
|
function verdictForSynthesis(comment, comments) {
|
|
130
185
|
const scan = scanCitations(comment.body);
|
|
131
186
|
const citations = scan.citations;
|
|
@@ -146,25 +201,15 @@ function verdictForSynthesis(comment, comments) {
|
|
|
146
201
|
renderIds(citations.map((row) => row.id)),
|
|
147
202
|
};
|
|
148
203
|
}
|
|
149
|
-
const citedTable = citations
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (citedTable === undefined) {
|
|
153
|
-
const claimed = citations.filter((row) => row.kind === "table");
|
|
154
|
-
if (claimed.length > 0) {
|
|
155
|
-
return {
|
|
156
|
-
status: "blocked",
|
|
157
|
-
reason: "missing-table-cite",
|
|
158
|
-
detail: "synthesis cites a verified-claims table id that is not a table on this thread: " +
|
|
159
|
-
renderIds(claimed.map((row) => row.id)),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
204
|
+
const citedTable = resolveCitedTable(citations, byCommentId);
|
|
205
|
+
if (!citedTable.ok) {
|
|
206
|
+
return { status: "blocked", reason: citedTable.reason, detail: citedTable.detail };
|
|
162
207
|
}
|
|
163
208
|
return {
|
|
164
209
|
status: "complete",
|
|
165
210
|
synthesisCommentId: comment.id,
|
|
166
211
|
citedLeanId: citedLean.id,
|
|
167
|
-
citedTableId: citedTable?.id ?? null,
|
|
212
|
+
citedTableId: citedTable.table?.id ?? null,
|
|
168
213
|
};
|
|
169
214
|
}
|
|
170
215
|
/**
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cooperative host-session identity and exact lifecycle-command rewriting (#3611).
|
|
3
3
|
*
|
|
4
|
-
* This is payload classification, not authentication. Host hook JSON
|
|
5
|
-
* forgeable under the occupancy model's cooperating-process
|
|
4
|
+
* This is payload/environment classification, not authentication. Host hook JSON
|
|
5
|
+
* is locally forgeable under the occupancy model's cooperating-process
|
|
6
|
+
* assumption. The provider table and the canonical owner form live in
|
|
7
|
+
* `session/host-session-owner.ts` because the CLI claim path resolves the same
|
|
8
|
+
* owner from the same host (#3873).
|
|
6
9
|
*/
|
|
7
|
-
|
|
8
|
-
export type HookHostIdentityProvider
|
|
9
|
-
export declare const MAX_HOOK_HOST_IDENTITY_UTF8_BYTES = 512;
|
|
10
|
+
import { ambientHostSessionOwner, HOST_IDENTITY_PROVIDERS, type HookHostIdentityProvider, type HookHostIdentitySource, hookHostIdentitySource, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES } from "../../session/host-session-owner.js";
|
|
11
|
+
export { ambientHostSessionOwner, HOST_IDENTITY_PROVIDERS, type HookHostIdentityProvider, type HookHostIdentitySource, hookHostIdentitySource, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, };
|
|
10
12
|
export type HookHostIdentityStatus = "ok" | "missing" | "invalid" | "conflict" | "unsupported";
|
|
11
13
|
export type HookHostIdentityResolution = {
|
|
12
14
|
readonly status: "ok";
|
|
@@ -24,12 +26,24 @@ export type HookHostIdentityResolution = {
|
|
|
24
26
|
/**
|
|
25
27
|
* Resolve the stable conversation/session-family key documented by each host.
|
|
26
28
|
*
|
|
27
|
-
* - Codex: `session_id` (parent and subagents share it).
|
|
28
|
-
* - Claude Code: `session_id` (`agent_id` is
|
|
29
|
-
* - Cursor: `conversation_id`; simultaneous `session_id` must agree.
|
|
30
|
-
* - Grok
|
|
29
|
+
* - Codex: payload `session_id` (parent and subagents share it).
|
|
30
|
+
* - Claude Code: payload `session_id` (`agent_id` is not the owner key).
|
|
31
|
+
* - Cursor: payload `conversation_id`; simultaneous `session_id` must agree.
|
|
32
|
+
* - Grok: hook process `GROK_SESSION_ID` (#3873). The payload `session_id` is
|
|
33
|
+
* deliberately not read -- that contract is unverified.
|
|
34
|
+
* - Unknown hosts: no guessed identity; callers keep the explicit/env flow.
|
|
31
35
|
*/
|
|
32
|
-
export declare function resolveHookHostIdentity(host: string, payload: unknown): HookHostIdentityResolution;
|
|
36
|
+
export declare function resolveHookHostIdentity(host: string, payload: unknown, environ?: NodeJS.ProcessEnv): HookHostIdentityResolution;
|
|
37
|
+
/**
|
|
38
|
+
* True when a `host-env` provider simply did not publish its variable (#3873).
|
|
39
|
+
*
|
|
40
|
+
* Absence is the pre-#3873 state rather than a broken contract, so callers keep
|
|
41
|
+
* the documented explicit `--session-id` / `DEFT_SESSION_ID` flow instead of
|
|
42
|
+
* failing a host that never had a hook identity to begin with. It is never an
|
|
43
|
+
* admission: with no explicit owner the actor stays unset and the write gate
|
|
44
|
+
* still denies. A present-but-malformed variable is `invalid` and fails closed.
|
|
45
|
+
*/
|
|
46
|
+
export declare function hostIdentityFallsBackToExplicitOwner(host: string, resolution: HookHostIdentityResolution): boolean;
|
|
33
47
|
export declare const EXACT_LIFECYCLE_VERBS: readonly ["session:start", "session:ready", "session:end", "occupancy:steal", "occupancy:release", "occupancy:heartbeat", "swarm:launch"];
|
|
34
48
|
export type ExactLifecycleVerb = (typeof EXACT_LIFECYCLE_VERBS)[number];
|
|
35
49
|
export interface ExactLifecycleCommandRewrite {
|
|
@@ -72,5 +86,4 @@ export declare function inspectExactLifecycleCommand(payload: unknown): ExactLif
|
|
|
72
86
|
* operator input.
|
|
73
87
|
*/
|
|
74
88
|
export declare function rewriteExactLifecycleCommand(payload: unknown, sessionId: string): ExactLifecycleCommandResult;
|
|
75
|
-
export {};
|
|
76
89
|
//# sourceMappingURL=host-session-identity.d.ts.map
|
|
@@ -1,47 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cooperative host-session identity and exact lifecycle-command rewriting (#3611).
|
|
3
3
|
*
|
|
4
|
-
* This is payload classification, not authentication. Host hook JSON
|
|
5
|
-
* forgeable under the occupancy model's cooperating-process
|
|
4
|
+
* This is payload/environment classification, not authentication. Host hook JSON
|
|
5
|
+
* is locally forgeable under the occupancy model's cooperating-process
|
|
6
|
+
* assumption. The provider table and the canonical owner form live in
|
|
7
|
+
* `session/host-session-owner.ts` because the CLI claim path resolves the same
|
|
8
|
+
* owner from the same host (#3873).
|
|
6
9
|
*/
|
|
10
|
+
import { ambientHostSessionOwner, canonicalHostSessionId, HOST_IDENTITY_PROVIDERS, hookHostIdentitySource, isUsableHostSessionId, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, readHostEnvIdentity, } from "../../session/host-session-owner.js";
|
|
7
11
|
import { isShellTool } from "../tools.js";
|
|
8
12
|
import { record, toolInputRecord } from "./payload.js";
|
|
9
13
|
import { hookToolName } from "./tool-name.js";
|
|
10
|
-
export
|
|
11
|
-
export const MAX_HOOK_HOST_IDENTITY_UTF8_BYTES = 512;
|
|
12
|
-
function hasControlCharacter(value) {
|
|
13
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
14
|
-
const unit = value.charCodeAt(index);
|
|
15
|
-
if (unit <= 0x1f || (unit >= 0x7f && unit <= 0x9f))
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
function hasUnpairedUtf16Surrogate(value) {
|
|
21
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
22
|
-
const unit = value.charCodeAt(index);
|
|
23
|
-
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
24
|
-
const next = value.charCodeAt(index + 1);
|
|
25
|
-
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
26
|
-
return true;
|
|
27
|
-
index += 1;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
14
|
+
export { ambientHostSessionOwner, HOST_IDENTITY_PROVIDERS, hookHostIdentitySource, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, };
|
|
35
15
|
function identityField(input, fieldName) {
|
|
36
16
|
if (!(fieldName in input))
|
|
37
17
|
return { status: "missing", value: null };
|
|
38
18
|
const raw = input[fieldName];
|
|
39
|
-
if (typeof raw !== "string" ||
|
|
40
|
-
raw.length === 0 ||
|
|
41
|
-
raw !== raw.trim() ||
|
|
42
|
-
hasControlCharacter(raw) ||
|
|
43
|
-
hasUnpairedUtf16Surrogate(raw) ||
|
|
44
|
-
Buffer.byteLength(raw, "utf8") > MAX_HOOK_HOST_IDENTITY_UTF8_BYTES) {
|
|
19
|
+
if (typeof raw !== "string" || !isUsableHostSessionId(raw)) {
|
|
45
20
|
return { status: "invalid", value: null };
|
|
46
21
|
}
|
|
47
22
|
return { status: "ok", value: raw };
|
|
@@ -49,10 +24,6 @@ function identityField(input, fieldName) {
|
|
|
49
24
|
function unresolvedHostIdentity(status, provider, message) {
|
|
50
25
|
return { status, provider, rawSessionId: null, sessionId: null, message };
|
|
51
26
|
}
|
|
52
|
-
function canonicalHostSessionId(provider, rawSessionId) {
|
|
53
|
-
const encoded = Buffer.from(rawSessionId, "utf8").toString("base64url");
|
|
54
|
-
return `host:${provider}:v1:${encoded}`;
|
|
55
|
-
}
|
|
56
27
|
function resolvedHostIdentity(provider, rawSessionId) {
|
|
57
28
|
return {
|
|
58
29
|
status: "ok",
|
|
@@ -65,28 +36,39 @@ function resolvedHostIdentity(provider, rawSessionId) {
|
|
|
65
36
|
/**
|
|
66
37
|
* Resolve the stable conversation/session-family key documented by each host.
|
|
67
38
|
*
|
|
68
|
-
* - Codex: `session_id` (parent and subagents share it).
|
|
69
|
-
* - Claude Code: `session_id` (`agent_id` is
|
|
70
|
-
* - Cursor: `conversation_id`; simultaneous `session_id` must agree.
|
|
71
|
-
* - Grok
|
|
39
|
+
* - Codex: payload `session_id` (parent and subagents share it).
|
|
40
|
+
* - Claude Code: payload `session_id` (`agent_id` is not the owner key).
|
|
41
|
+
* - Cursor: payload `conversation_id`; simultaneous `session_id` must agree.
|
|
42
|
+
* - Grok: hook process `GROK_SESSION_ID` (#3873). The payload `session_id` is
|
|
43
|
+
* deliberately not read -- that contract is unverified.
|
|
44
|
+
* - Unknown hosts: no guessed identity; callers keep the explicit/env flow.
|
|
72
45
|
*/
|
|
73
|
-
export function resolveHookHostIdentity(host, payload) {
|
|
74
|
-
|
|
75
|
-
|
|
46
|
+
export function resolveHookHostIdentity(host, payload, environ = process.env) {
|
|
47
|
+
const source = hookHostIdentitySource(host);
|
|
48
|
+
if (source === null) {
|
|
49
|
+
return unresolvedHostIdentity("unsupported", host, `Host ${host || "<empty>"} has no verified session identity contract.`);
|
|
76
50
|
}
|
|
77
51
|
const provider = host;
|
|
52
|
+
if (source.kind === "host-env") {
|
|
53
|
+
const value = readHostEnvIdentity(environ, source.variable);
|
|
54
|
+
if (value.status !== "ok") {
|
|
55
|
+
return unresolvedHostIdentity(value.status, provider, `${provider} hook process environment ` +
|
|
56
|
+
`${value.status === "missing" ? "omits" : "has invalid"} ${source.variable}.`);
|
|
57
|
+
}
|
|
58
|
+
return resolvedHostIdentity(provider, value.rawSessionId);
|
|
59
|
+
}
|
|
78
60
|
const input = record(payload);
|
|
79
61
|
if (input === null) {
|
|
80
62
|
return unresolvedHostIdentity("invalid", provider, `${provider} hook payload is not an object.`);
|
|
81
63
|
}
|
|
82
|
-
if (provider
|
|
83
|
-
const session = identityField(input,
|
|
64
|
+
if (provider !== "cursor") {
|
|
65
|
+
const session = identityField(input, source.field);
|
|
84
66
|
if (session.status !== "ok") {
|
|
85
|
-
return unresolvedHostIdentity(session.status, provider, `${provider} hook payload ${session.status === "missing" ? "omits" : "has invalid"}
|
|
67
|
+
return unresolvedHostIdentity(session.status, provider, `${provider} hook payload ${session.status === "missing" ? "omits" : "has invalid"} ${source.field}.`);
|
|
86
68
|
}
|
|
87
69
|
return resolvedHostIdentity(provider, session.value);
|
|
88
70
|
}
|
|
89
|
-
const conversation = identityField(input,
|
|
71
|
+
const conversation = identityField(input, source.field);
|
|
90
72
|
if (conversation.status !== "ok") {
|
|
91
73
|
return unresolvedHostIdentity(conversation.status, provider, `Cursor hook payload ${conversation.status === "missing" ? "omits" : "has invalid"} conversation_id.`);
|
|
92
74
|
}
|
|
@@ -99,6 +81,18 @@ export function resolveHookHostIdentity(host, payload) {
|
|
|
99
81
|
}
|
|
100
82
|
return resolvedHostIdentity(provider, conversation.value);
|
|
101
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* True when a `host-env` provider simply did not publish its variable (#3873).
|
|
86
|
+
*
|
|
87
|
+
* Absence is the pre-#3873 state rather than a broken contract, so callers keep
|
|
88
|
+
* the documented explicit `--session-id` / `DEFT_SESSION_ID` flow instead of
|
|
89
|
+
* failing a host that never had a hook identity to begin with. It is never an
|
|
90
|
+
* admission: with no explicit owner the actor stays unset and the write gate
|
|
91
|
+
* still denies. A present-but-malformed variable is `invalid` and fails closed.
|
|
92
|
+
*/
|
|
93
|
+
export function hostIdentityFallsBackToExplicitOwner(host, resolution) {
|
|
94
|
+
return resolution.status === "missing" && hookHostIdentitySource(host)?.kind === "host-env";
|
|
95
|
+
}
|
|
102
96
|
export const EXACT_LIFECYCLE_VERBS = [
|
|
103
97
|
"session:start",
|
|
104
98
|
"session:ready",
|
|
@@ -117,7 +111,10 @@ const DIRECT_LIFECYCLE_VERBS = {
|
|
|
117
111
|
"occupancy:heartbeat": "occupancy:heartbeat",
|
|
118
112
|
"swarm-launch": "swarm:launch",
|
|
119
113
|
};
|
|
120
|
-
|
|
114
|
+
// Derived from the provider list so the rewrite surface cannot drift from the
|
|
115
|
+
// identity surface: a provider added to one is added to both (#3873). Provider
|
|
116
|
+
// ids are lowercase ASCII words, so the alternation needs no escaping.
|
|
117
|
+
const CANONICAL_OWNER_PATTERN = new RegExp(`^host:(?:${HOST_IDENTITY_PROVIDERS.join("|")}):v1:[A-Za-z0-9_-]+$`);
|
|
121
118
|
// Shell expansion markers are deliberately absent: `$`/backticks for POSIX,
|
|
122
119
|
// `@` splatting for PowerShell, and `%NAME%` expansion for command shells.
|
|
123
120
|
// Backslashes are inspectable so Windows path-bearing lifecycle commands fail
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Policy / ritual / scope / permission emission stay in dispatcher + CLI adapter.
|
|
6
6
|
*/
|
|
7
7
|
export { classifyHookPayload } from "./classify.js";
|
|
8
|
-
export { EXACT_LIFECYCLE_VERBS, type ExactLifecycleCommandConflict, type ExactLifecycleCommandInspection, type ExactLifecycleCommandResult, type ExactLifecycleCommandRewrite, type ExactLifecycleVerb, exactLifecycleCommandVerb, HOST_IDENTITY_PROVIDERS, type HookHostIdentityProvider, type HookHostIdentityResolution, type HookHostIdentityStatus, inspectExactLifecycleCommand, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, resolveHookHostIdentity, rewriteExactLifecycleCommand, } from "./host-session-identity.js";
|
|
9
|
-
export { hookApplyPatchBodyPaths, hookMcpArgsText, hookMutationTargetPaths, hookPathSet, hookShellCommand, hookWriteTargetPath, } from "./paths.js";
|
|
8
|
+
export { EXACT_LIFECYCLE_VERBS, type ExactLifecycleCommandConflict, type ExactLifecycleCommandInspection, type ExactLifecycleCommandResult, type ExactLifecycleCommandRewrite, type ExactLifecycleVerb, exactLifecycleCommandVerb, HOST_IDENTITY_PROVIDERS, type HookHostIdentityProvider, type HookHostIdentityResolution, type HookHostIdentitySource, type HookHostIdentityStatus, hookHostIdentitySource, hostIdentityFallsBackToExplicitOwner, inspectExactLifecycleCommand, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, resolveHookHostIdentity, rewriteExactLifecycleCommand, } from "./host-session-identity.js";
|
|
9
|
+
export { hookApplyPatchBodyPaths, hookApplyPatchBodyText, hookMcpArgsText, hookMutationTargetPaths, hookPathSet, hookShellCommand, hookWriteTargetPath, } from "./paths.js";
|
|
10
10
|
export { fieldPresent, fieldString, firstString, hookPayloadTopLevelKeys, record, toolInputRecord, } from "./payload.js";
|
|
11
11
|
export { applyPatchMutationPaths, parseHookStdin, stripUtf8Bom } from "./stdin.js";
|
|
12
12
|
export { hookToolName, inferCursorDirectWriteToolName, type MissingToolNameInput, missingToolNameMessage, } from "./tool-name.js";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Policy / ritual / scope / permission emission stay in dispatcher + CLI adapter.
|
|
6
6
|
*/
|
|
7
7
|
export { classifyHookPayload } from "./classify.js";
|
|
8
|
-
export { EXACT_LIFECYCLE_VERBS, exactLifecycleCommandVerb, HOST_IDENTITY_PROVIDERS, inspectExactLifecycleCommand, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, resolveHookHostIdentity, rewriteExactLifecycleCommand, } from "./host-session-identity.js";
|
|
9
|
-
export { hookApplyPatchBodyPaths, hookMcpArgsText, hookMutationTargetPaths, hookPathSet, hookShellCommand, hookWriteTargetPath, } from "./paths.js";
|
|
8
|
+
export { EXACT_LIFECYCLE_VERBS, exactLifecycleCommandVerb, HOST_IDENTITY_PROVIDERS, hookHostIdentitySource, hostIdentityFallsBackToExplicitOwner, inspectExactLifecycleCommand, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, resolveHookHostIdentity, rewriteExactLifecycleCommand, } from "./host-session-identity.js";
|
|
9
|
+
export { hookApplyPatchBodyPaths, hookApplyPatchBodyText, hookMcpArgsText, hookMutationTargetPaths, hookPathSet, hookShellCommand, hookWriteTargetPath, } from "./paths.js";
|
|
10
10
|
export { fieldPresent, fieldString, firstString, hookPayloadTopLevelKeys, record, toolInputRecord, } from "./payload.js";
|
|
11
11
|
export { applyPatchMutationPaths, parseHookStdin, stripUtf8Bom } from "./stdin.js";
|
|
12
12
|
export { hookToolName, inferCursorDirectWriteToolName, missingToolNameMessage, } from "./tool-name.js";
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
|
|
8
8
|
*/
|
|
9
9
|
export declare function hookWriteTargetPath(payload: unknown): string | null;
|
|
10
|
+
/** Raw ApplyPatch body text from patch / unified_diff / diff fields. */
|
|
11
|
+
export declare function hookApplyPatchBodyText(payload: unknown): string | null;
|
|
10
12
|
/** ApplyPatch body paths from patch / unified_diff / diff fields. */
|
|
11
13
|
export declare function hookApplyPatchBodyPaths(payload: unknown): string[];
|
|
12
14
|
/**
|
|
@@ -22,13 +22,13 @@ export function hookWriteTargetPath(payload) {
|
|
|
22
22
|
input.path,
|
|
23
23
|
]);
|
|
24
24
|
}
|
|
25
|
-
/** ApplyPatch body
|
|
26
|
-
export function
|
|
25
|
+
/** Raw ApplyPatch body text from patch / unified_diff / diff fields. */
|
|
26
|
+
export function hookApplyPatchBodyText(payload) {
|
|
27
27
|
const input = record(payload);
|
|
28
28
|
if (input === null)
|
|
29
|
-
return
|
|
29
|
+
return null;
|
|
30
30
|
const toolInput = toolInputRecord(input);
|
|
31
|
-
|
|
31
|
+
return firstString([
|
|
32
32
|
toolInput?.patch,
|
|
33
33
|
toolInput?.unified_diff,
|
|
34
34
|
toolInput?.diff,
|
|
@@ -36,6 +36,10 @@ export function hookApplyPatchBodyPaths(payload) {
|
|
|
36
36
|
input.unified_diff,
|
|
37
37
|
input.diff,
|
|
38
38
|
]);
|
|
39
|
+
}
|
|
40
|
+
/** ApplyPatch body paths from patch / unified_diff / diff fields. */
|
|
41
|
+
export function hookApplyPatchBodyPaths(payload) {
|
|
42
|
+
const patch = hookApplyPatchBodyText(payload);
|
|
39
43
|
return patch === null ? [] : applyPatchMutationPaths(patch);
|
|
40
44
|
}
|
|
41
45
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Pure stdin → payload parse for hook-dispatch (#2734 / #2738 / #2950).
|
|
3
3
|
* No process I/O — operates on an already-read string.
|
|
4
4
|
*/
|
|
5
|
+
import { firstString, record, toolInputRecord } from "./payload.js";
|
|
5
6
|
const UTF8_BOM = "\uFEFF";
|
|
6
7
|
const APPLY_PATCH_BEGIN_MARKER = "*** Begin Patch";
|
|
7
8
|
/** Single-file Add/Update only — other *** … File: ops must fail closed (#2738 Greptile). */
|
|
@@ -9,8 +10,9 @@ const APPLY_PATCH_MUTATION_LINE_RE = /^\*\*\* (Add File|Update File|Delete File|
|
|
|
9
10
|
/**
|
|
10
11
|
* Canonical apply_patch spells a rename as `*** Update File:` followed by
|
|
11
12
|
* `*** Move to:`. The destination is a mutation target in its own right, so it
|
|
12
|
-
* must reach root admission
|
|
13
|
-
*
|
|
13
|
+
* must reach root admission. Synthesis refuses when applyPatchMutationPaths
|
|
14
|
+
* reports more than one unique path, so a Move-to destination is not dropped
|
|
15
|
+
* from the single-target contract (#3614 / #3794).
|
|
14
16
|
*/
|
|
15
17
|
const APPLY_PATCH_MOVE_DESTINATION_RE = /^\*\*\* Move to: (.+)$/gm;
|
|
16
18
|
export function stripUtf8Bom(raw) {
|
|
@@ -49,6 +51,10 @@ function trySynthesizeFreeFormApplyPatch(normalized) {
|
|
|
49
51
|
}
|
|
50
52
|
if (mutations.length !== 1)
|
|
51
53
|
return null;
|
|
54
|
+
// Count headers plus Move-to destinations so a rename cannot collapse to one
|
|
55
|
+
// checked path (#3614 / #3794). Reuses applyPatchMutationPaths; no second parser.
|
|
56
|
+
if (applyPatchMutationPaths(normalized).length !== 1)
|
|
57
|
+
return null;
|
|
52
58
|
const sole = mutations[0];
|
|
53
59
|
if (sole === undefined || (sole.op !== "Add File" && sole.op !== "Update File"))
|
|
54
60
|
return null;
|
|
@@ -63,6 +69,65 @@ function trySynthesizeFreeFormApplyPatch(normalized) {
|
|
|
63
69
|
context: {},
|
|
64
70
|
};
|
|
65
71
|
}
|
|
72
|
+
function declaredWritePathFromParsed(payload) {
|
|
73
|
+
const input = record(payload);
|
|
74
|
+
if (input === null)
|
|
75
|
+
return null;
|
|
76
|
+
const toolInput = toolInputRecord(input);
|
|
77
|
+
return firstString([
|
|
78
|
+
toolInput?.file_path,
|
|
79
|
+
toolInput?.filePath,
|
|
80
|
+
toolInput?.path,
|
|
81
|
+
input.file_path,
|
|
82
|
+
input.filePath,
|
|
83
|
+
input.path,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
function applyPatchBodyTextFromParsed(payload) {
|
|
87
|
+
const input = record(payload);
|
|
88
|
+
if (input === null)
|
|
89
|
+
return null;
|
|
90
|
+
const toolInput = toolInputRecord(input);
|
|
91
|
+
return firstString([
|
|
92
|
+
toolInput?.patch,
|
|
93
|
+
toolInput?.unified_diff,
|
|
94
|
+
toolInput?.diff,
|
|
95
|
+
input.patch,
|
|
96
|
+
input.unified_diff,
|
|
97
|
+
input.diff,
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
function withToolInputPath(parsed, path) {
|
|
101
|
+
const input = record(parsed);
|
|
102
|
+
if (input === null)
|
|
103
|
+
return parsed;
|
|
104
|
+
for (const key of ["tool_input", "toolInput", "input", "arguments"]) {
|
|
105
|
+
const nested = record(input[key]);
|
|
106
|
+
if (nested !== null) {
|
|
107
|
+
return { ...input, [key]: { ...nested, path } };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ...input, tool_input: { path } };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Valid-JSON hosts (Codex) never hit the JSON.parse catch arm, so the free-form
|
|
114
|
+
* extractor was unreached. Call it on the patch body when no path was declared.
|
|
115
|
+
* Does not replace the parsed payload; only fills tool_input.path.
|
|
116
|
+
*/
|
|
117
|
+
function attachSynthesizedApplyPatchPath(parsed) {
|
|
118
|
+
if (declaredWritePathFromParsed(parsed) !== null)
|
|
119
|
+
return parsed;
|
|
120
|
+
const patch = applyPatchBodyTextFromParsed(parsed);
|
|
121
|
+
if (patch === null)
|
|
122
|
+
return parsed;
|
|
123
|
+
const synthesized = trySynthesizeFreeFormApplyPatch(patch);
|
|
124
|
+
if (synthesized === null)
|
|
125
|
+
return parsed;
|
|
126
|
+
const synthesizedPath = declaredWritePathFromParsed(synthesized.payload);
|
|
127
|
+
if (synthesizedPath === null)
|
|
128
|
+
return parsed;
|
|
129
|
+
return withToolInputPath(parsed, synthesizedPath);
|
|
130
|
+
}
|
|
66
131
|
/**
|
|
67
132
|
* Parse host hook stdin text into a payload + parse context.
|
|
68
133
|
* Empty → stdinEmpty; invalid JSON without free-form ApplyPatch → parseFailed.
|
|
@@ -76,7 +141,8 @@ export function parseHookStdin(raw) {
|
|
|
76
141
|
return { payload: {}, context: { stdinEmpty: true } };
|
|
77
142
|
}
|
|
78
143
|
try {
|
|
79
|
-
|
|
144
|
+
const parsed = JSON.parse(normalized);
|
|
145
|
+
return { payload: attachSynthesizedApplyPatchPath(parsed), context: {} };
|
|
80
146
|
}
|
|
81
147
|
catch {
|
|
82
148
|
const synthesized = trySynthesizeFreeFormApplyPatch(normalized);
|
|
@@ -12,13 +12,27 @@
|
|
|
12
12
|
* *forms* instead, and see `content/contracts/path-write-fence.md`.
|
|
13
13
|
*
|
|
14
14
|
* Residual known-open (recognition, not resolution): `python -c`,
|
|
15
|
-
* `cmd /c copy`, obfuscated `bash -c`.
|
|
15
|
+
* `cmd /c copy`, obfuscated `bash -c`. Tree-wide destructive git
|
|
16
|
+
* (`reset --hard`, `clean -f`, `checkout -f`/`switch -f`, `stash drop`/`clear`)
|
|
17
|
+
* is classified separately by {@link classifyGitDestructive} and is always
|
|
18
|
+
* fail-closed at the dispatcher, independent of `shellDestForms`.
|
|
16
19
|
*
|
|
17
20
|
* Commit-time active-scope is out of this slice (#3438), and ⊗ do not weaken
|
|
18
21
|
* Edit to match Shell. Keep this sentence on ONE line: acceptance clause 4 is
|
|
19
22
|
* verified by a literal token search, so wrapping it silently fails the gate.
|
|
20
23
|
*/
|
|
21
24
|
export type ProductDestFormKind = "git-checkout" | "git-restore" | "rm" | "rmdir";
|
|
25
|
+
export type GitDestructiveKind = "git-reset-hard" | "git-clean" | "git-checkout-force" | "git-stash-drop";
|
|
26
|
+
export interface GitDestructiveForm {
|
|
27
|
+
readonly kind: GitDestructiveKind;
|
|
28
|
+
/** Relocating context paths from `-C` / `--git-dir` / `--work-tree` / `GIT_DIR` / `GIT_WORK_TREE`. */
|
|
29
|
+
readonly relocators: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* True when the command is compound or the relocator is opaque (`GIT_CONFIG*`,
|
|
32
|
+
* `-c core.workTree`). The dispatcher denies these; it cannot prove a fixture.
|
|
33
|
+
*/
|
|
34
|
+
readonly unprovable: boolean;
|
|
35
|
+
}
|
|
22
36
|
export interface ProductDestForm {
|
|
23
37
|
readonly kind: ProductDestFormKind;
|
|
24
38
|
readonly path: string;
|
|
@@ -43,4 +57,9 @@ export declare function payloadWithInjectedWriteTarget(payload: unknown, destPat
|
|
|
43
57
|
*/
|
|
44
58
|
export declare function destFormHasExpansion(path: string): boolean;
|
|
45
59
|
export declare function classifyProductDestForms(command: string): ProductDestForm[];
|
|
60
|
+
/**
|
|
61
|
+
* Tree-wide destructive git — always-on at the dispatcher, not behind
|
|
62
|
+
* `shellDestForms`. Empty when the command is not one of those forms.
|
|
63
|
+
*/
|
|
64
|
+
export declare function classifyGitDestructive(command: string): GitDestructiveForm[];
|
|
46
65
|
//# sourceMappingURL=dest-form.d.ts.map
|