codictator 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-packs/core/fixtures/conventional-commit/passing/basic/case.json +1 -1
- package/dist/builtin-packs/core/fixtures/conventional-commit/violating/basic/case.json +1 -1
- package/dist/builtin-packs/typescript/fixtures/lint/passing/exported/case.json +8 -0
- package/dist/builtin-packs/typescript/pack.json +1 -1
- package/dist/cli/gate-scope.d.ts +1 -0
- package/dist/cli/gate-scope.js +11 -0
- package/dist/cli/git-hook.js +13 -8
- package/dist/cli/plan-launcher.d.ts +1 -0
- package/dist/cli/plan-launcher.js +53 -3
- package/dist/cli/task-advance.js +16 -16
- package/dist/features/adapters/claude-shaped.d.ts +1 -0
- package/dist/features/adapters/claude-shaped.js +29 -31
- package/dist/features/adapters/copilot.js +22 -29
- package/dist/features/audit/append.js +9 -4
- package/dist/features/audit/surfaces.js +15 -8
- package/dist/features/ci/protection.js +37 -13
- package/dist/features/deps/npm.js +39 -24
- package/dist/features/deps/registry.js +45 -34
- package/dist/features/deps/stable.js +52 -27
- package/dist/features/deps/upgrade.d.ts +1 -1
- package/dist/features/deps/upgrade.js +29 -19
- package/dist/features/engine/evaluate.js +6 -1
- package/dist/features/packs/enforcers/ci-guards.js +4 -1
- package/dist/features/packs/enforcers/code.js +2 -1
- package/dist/features/packs/enforcers/engine-version-guards.d.ts +6 -0
- package/dist/features/packs/enforcers/engine-version-guards.js +42 -0
- package/dist/features/packs/enforcers/external-bind.js +62 -28
- package/dist/features/packs/enforcers/external-sarif.js +30 -24
- package/dist/features/packs/enforcers/gate-guards.d.ts +0 -1
- package/dist/features/packs/enforcers/gate-guards.js +83 -136
- package/dist/features/packs/enforcers/health-measures.d.ts +2 -0
- package/dist/features/packs/enforcers/health-measures.js +10 -1
- package/dist/features/packs/enforcers/health-refs.d.ts +1 -2
- package/dist/features/packs/enforcers/health-refs.js +71 -56
- package/dist/features/packs/enforcers/secret-scan.d.ts +4 -20
- package/dist/features/packs/enforcers/secret-scan.js +61 -67
- package/dist/features/packs/enforcers/ts-checks.js +104 -58
- package/dist/features/packs/enforcers/ts-security-patterns.js +12 -8
- package/dist/features/packs/manifest-decl.d.ts +13 -0
- package/dist/features/packs/manifest-decl.js +121 -0
- package/dist/features/packs/manifest.d.ts +2 -12
- package/dist/features/packs/manifest.js +5 -90
- package/dist/features/packs/selftest.js +22 -5
- package/dist/features/remediation/adoption.js +40 -29
- package/dist/features/testing/scenarios.js +49 -27
- package/dist/features/testing/tap-parser.js +38 -26
- package/dist/features/workflow/phases.js +45 -42
- package/dist/features/workflow/review.js +17 -10
- package/dist/kernel/git.js +4 -1
- package/dist/kernel/pattern.js +120 -86
- package/dist/kernel/secret-paths.js +34 -20
- package/dist/kernel/shell-cmd.js +23 -6
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/cli/gate-scope.d.ts
CHANGED
|
@@ -7,4 +7,5 @@ export interface GateScope {
|
|
|
7
7
|
readonly note: string;
|
|
8
8
|
readonly proposedContent?: string;
|
|
9
9
|
}
|
|
10
|
+
export declare function addedLines(diff: string): string;
|
|
10
11
|
export declare function evaluateGateScope(ports: CommandPorts, packIds: ReadonlyArray<string>, scope: GateScope, now: string, configHash: string): Promise<Result<Array<Finding>>>;
|
package/dist/cli/gate-scope.js
CHANGED
|
@@ -51,6 +51,17 @@ function mergeGateFindings(first, second) {
|
|
|
51
51
|
}
|
|
52
52
|
return merged.sort(compareGateFindings);
|
|
53
53
|
}
|
|
54
|
+
// BUG-0022 + SPEC §4.7.1: the L3 gate judges added lines only, so a commit
|
|
55
|
+
// deleting a flagged literal passes while staging one still blocks. A diff
|
|
56
|
+
// carries its removed lines as `-` rows; scanning the raw diff would match
|
|
57
|
+
// those and make the removal uncommittable.
|
|
58
|
+
export function addedLines(diff) {
|
|
59
|
+
return diff
|
|
60
|
+
.split("\n")
|
|
61
|
+
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
|
62
|
+
.map((line) => line.slice(1))
|
|
63
|
+
.join("\n");
|
|
64
|
+
}
|
|
54
65
|
// BUG-0014 + SPEC §4.3.2: the gate evaluation over a diff scope. The
|
|
55
66
|
// pre-tool leg keeps today's fail-closed local verdict byte for byte; the
|
|
56
67
|
// review leg adds the diff-sound review subset over the same files, so the
|
package/dist/cli/git-hook.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// the change with the gate rule subset `verify` loads over staged scopes —
|
|
5
5
|
// new scope plumbing, no new detection logic. commit-msg appends
|
|
6
6
|
// the Codictator-L3 trailer §4.14.2 checks at L4.
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import { ENGINE_VERSION, HOOK_MARKER } from "../features/config/index.js";
|
|
8
9
|
import { err, makeError, ok, readHostText, writeHostText, } from "../kernel/index.js";
|
|
9
|
-
import { evaluateGateScope } from "./gate-scope.js";
|
|
10
|
+
import { addedLines, evaluateGateScope } from "./gate-scope.js";
|
|
10
11
|
import { printFinding } from "./inspect.js";
|
|
11
12
|
import { commandEnv, fail, loadRepoConfig, MIN_NODE_MAJOR, needRoot, nodeMajor, nodeTooOldMessage, printHuman, printJson, reportError, usageError, } from "./shared.js";
|
|
12
13
|
const GIT_TIMEOUT_MS = 30000;
|
|
@@ -53,7 +54,7 @@ async function evaluateGitChange(ports, req, hook, packIds, files, content, now,
|
|
|
53
54
|
const evaluated = await evaluateGateScope(ports, packIds, {
|
|
54
55
|
files: [...files],
|
|
55
56
|
note: hook,
|
|
56
|
-
...(content === "" ? {} : { proposedContent: content }),
|
|
57
|
+
...(content === "" ? {} : { proposedContent: addedLines(content) }),
|
|
57
58
|
}, now, configHash);
|
|
58
59
|
if (!evaluated.ok) {
|
|
59
60
|
return reportError(evaluated.error, req.json);
|
|
@@ -77,16 +78,20 @@ async function runPreCommit(req, ports, packIds, now, configHash) {
|
|
|
77
78
|
}
|
|
78
79
|
return evaluateGitChange(ports, req, "pre-commit", packIds, staged.value, diff.value, now, configHash);
|
|
79
80
|
}
|
|
80
|
-
// SPEC §4.14.4: git hands commit-msg
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
81
|
+
// SPEC §4.14.4: git hands commit-msg the COMMIT_EDITMSG path, absolute in a
|
|
82
|
+
// linked worktree (under the main repo's .git/worktrees/<name>/ dir, outside
|
|
83
|
+
// the worktree root) but relative to the repo root (`.git/COMMIT_EDITMSG`)
|
|
84
|
+
// in the main worktree. The confined Fs port rejects absolute paths
|
|
85
|
+
// (CDX-E701), so the hook uses kernel-owned host I/O — which itself
|
|
86
|
+
// requires absolute paths — resolving a relative handoff against the
|
|
87
|
+
// invocation cwd first; an unreadable or unwritable message file blocks
|
|
88
|
+
// fail-closed.
|
|
85
89
|
async function runCommitMsg(req, configHash) {
|
|
86
90
|
if (req.args.length !== 2 || req.args[1] === undefined) {
|
|
87
91
|
return usageError("usage: codictator git commit-msg <file>");
|
|
88
92
|
}
|
|
89
|
-
const
|
|
93
|
+
const raw = req.args[1];
|
|
94
|
+
const file = path.isAbsolute(raw) ? raw : path.resolve(req.cwd, raw);
|
|
90
95
|
const trailer = `Codictator-L3: ${configHash.slice(0, 12)}`;
|
|
91
96
|
const read = await readHostText(file);
|
|
92
97
|
if (!read.ok) {
|
|
@@ -20,7 +20,11 @@ export const LAUNCHER_TEXT = [
|
|
|
20
20
|
"// any newer syntax could load, and only node: built-ins are imported. It",
|
|
21
21
|
"// sets CODICTATOR_HOOK=1 in the engine process, then resolves the engine at",
|
|
22
22
|
"// <root>/node_modules/codictator/dist/cli/main.js else",
|
|
23
|
-
"// <root>/.codictator/tool/node_modules/codictator/dist/cli/main.js.
|
|
23
|
+
"// <root>/.codictator/tool/node_modules/codictator/dist/cli/main.js. The",
|
|
24
|
+
"// launcher reads the resolved engine's manifest version and passes it",
|
|
25
|
+
"// as main's third argument, so hook validation sees the installed",
|
|
26
|
+
"// version (SPEC §4.2.1 EC-11); an unreadable manifest falls back to",
|
|
27
|
+
"// the two-argument call. A",
|
|
24
28
|
"// missing engine answers the harness's embedded static CDX-E003 block",
|
|
25
29
|
"// reply; outside hook it prints CDX-E003 and exits 6. Git hooks arrive",
|
|
26
30
|
"// as a `git` first word and route to the engine like hook words do, so",
|
|
@@ -28,7 +32,7 @@ export const LAUNCHER_TEXT = [
|
|
|
28
32
|
"// Version arbitration",
|
|
29
33
|
"// between the root and tool-dir engines (SPEC §4.19 EC-6) belongs to the",
|
|
30
34
|
"// phase 9 CLI: until then the first candidate that exists wins.",
|
|
31
|
-
launcherImport("import { existsSync }", "fs"),
|
|
35
|
+
launcherImport("import { existsSync, readFileSync }", "fs"),
|
|
32
36
|
launcherImport("import path", "path"),
|
|
33
37
|
launcherImport("import { fileURLToPath, pathToFileURL }", "url"),
|
|
34
38
|
"",
|
|
@@ -141,6 +145,48 @@ export const LAUNCHER_TEXT = [
|
|
|
141
145
|
"\treturn null;",
|
|
142
146
|
"}",
|
|
143
147
|
"",
|
|
148
|
+
"// SPEC §4.2.1 EC-11: the engine entry validates config.codictator",
|
|
149
|
+
"// against the installed engine version, so the launcher extracts it",
|
|
150
|
+
"// from the resolved engine's own manifest text and passes it as",
|
|
151
|
+
"// main's third argument. A version-field scan keeps JSON parsing in",
|
|
152
|
+
"// the kernel per §10.3 item 6 (and a regex literal would confuse the",
|
|
153
|
+
"// blankCode lexer the ts/lint gate runs on); any unreadable or",
|
|
154
|
+
"// version-less manifest answers null and the caller falls back to",
|
|
155
|
+
"// the two-argument call, which keeps the pinned placeholder —",
|
|
156
|
+
"// fail-closed downstream, never an allow.",
|
|
157
|
+
"function installedEngineVersion(enginePath) {",
|
|
158
|
+
"\ttry {",
|
|
159
|
+
"\t\tconst manifestPath = path.join(",
|
|
160
|
+
"\t\t\tpath.dirname(path.dirname(path.dirname(enginePath))),",
|
|
161
|
+
`\t\t\t"package.json",`,
|
|
162
|
+
"\t\t);",
|
|
163
|
+
`\t\tconst text = readFileSync(manifestPath, "utf8");`,
|
|
164
|
+
'\t\tconst marker = "\\"version\\":";',
|
|
165
|
+
"\t\tconst at = text.indexOf(marker);",
|
|
166
|
+
"\t\tif (at === -1) {",
|
|
167
|
+
"\t\t\treturn null;",
|
|
168
|
+
"\t\t}",
|
|
169
|
+
"\t\tlet index = at + marker.length;",
|
|
170
|
+
'\t\twhile (text[index] === " " || text[index] === "\\t" || text[index] === "\\n" || text[index] === "\\r") {',
|
|
171
|
+
"\t\t\tindex += 1;",
|
|
172
|
+
"\t\t}",
|
|
173
|
+
'\t\tif (text[index] !== "\\"") {',
|
|
174
|
+
"\t\t\treturn null;",
|
|
175
|
+
"\t\t}",
|
|
176
|
+
'\t\tconst end = text.indexOf("\\"", index + 1);',
|
|
177
|
+
"\t\tif (end === -1) {",
|
|
178
|
+
"\t\t\treturn null;",
|
|
179
|
+
"\t\t}",
|
|
180
|
+
"\t\tconst version = text.slice(index + 1, end);",
|
|
181
|
+
'\t\tif (version === "") {',
|
|
182
|
+
"\t\t\treturn null;",
|
|
183
|
+
"\t\t}",
|
|
184
|
+
"\t\treturn version;",
|
|
185
|
+
"\t} catch (ignored) {",
|
|
186
|
+
"\t\treturn null;",
|
|
187
|
+
"\t}",
|
|
188
|
+
"}",
|
|
189
|
+
"",
|
|
144
190
|
"function reportMissing(harness) {",
|
|
145
191
|
"\tconst missing = staticReply(",
|
|
146
192
|
"\t\tharness,",
|
|
@@ -175,7 +221,11 @@ export const LAUNCHER_TEXT = [
|
|
|
175
221
|
`\t\t\t\tif (engine === null || typeof engine.main !== "function") {`,
|
|
176
222
|
`\t\t\t\t\tthrow new Error("engine entry has no main");`,
|
|
177
223
|
"\t\t\t\t}",
|
|
178
|
-
"\t\t\t\
|
|
224
|
+
"\t\t\t\tconst version = installedEngineVersion(enginePath);",
|
|
225
|
+
"\t\t\t\tif (version === null) {",
|
|
226
|
+
"\t\t\t\t\treturn engine.main(args, process.cwd());",
|
|
227
|
+
"\t\t\t\t}",
|
|
228
|
+
"\t\t\t\treturn engine.main(args, process.cwd(), version);",
|
|
179
229
|
"\t\t\t},",
|
|
180
230
|
"\t\t\t(error) => {",
|
|
181
231
|
"\t\t\t\tthrow error;",
|
package/dist/cli/task-advance.js
CHANGED
|
@@ -87,31 +87,31 @@ async function taskMutate(req, root, sub) {
|
|
|
87
87
|
}
|
|
88
88
|
return taskClose(req, root, id);
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
const sub = req.args[0] ?? "";
|
|
98
|
-
if (sub === "new") {
|
|
99
|
-
return taskNew(req, root);
|
|
100
|
-
}
|
|
101
|
-
if (sub === "show") {
|
|
102
|
-
return taskShow(req, root);
|
|
103
|
-
}
|
|
90
|
+
const SINGLE_SUBCOMMANDS = {
|
|
91
|
+
new: (req, root) => taskNew(req, root),
|
|
92
|
+
show: (req, root) => taskShow(req, root),
|
|
93
|
+
checkpoint: (req, root) => taskCheckpoint(req, root),
|
|
94
|
+
};
|
|
95
|
+
async function runTaskSub(req, root, sub) {
|
|
104
96
|
if (sub === "phase" && req.args[1] === "next") {
|
|
105
97
|
return taskPhaseNext(req, root);
|
|
106
98
|
}
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
const single = SINGLE_SUBCOMMANDS[sub];
|
|
100
|
+
if (single !== undefined) {
|
|
101
|
+
return single(req, root);
|
|
109
102
|
}
|
|
110
103
|
if (sub === "ack" || sub === "amend" || sub === "close") {
|
|
111
104
|
return taskMutate(req, root, sub);
|
|
112
105
|
}
|
|
113
106
|
return usageError("usage: codictator task <new|show|amend|phase next|checkpoint|close|ack> ...");
|
|
114
107
|
}
|
|
108
|
+
export async function handleTask(req) {
|
|
109
|
+
const root = await needTaskRoot(req);
|
|
110
|
+
if (typeof root !== "string") {
|
|
111
|
+
return root;
|
|
112
|
+
}
|
|
113
|
+
return runTaskSub(req, root, req.args[0] ?? "");
|
|
114
|
+
}
|
|
115
115
|
async function taskShow(req, root) {
|
|
116
116
|
const ids = await listTaskIds(root);
|
|
117
117
|
if (req.args.length === 1) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Result } from "../../kernel/index.ts";
|
|
2
2
|
import type { HookEvent, ToolKind } from "./contract.ts";
|
|
3
|
+
export declare function readString(record: Record<string, unknown>, keys: ReadonlyArray<string>): string | undefined;
|
|
3
4
|
export declare function parseClaudeShapedEvent(harness: string, event: string, raw: unknown, table: Readonly<Record<string, ToolKind>>): Result<HookEvent>;
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
// leaks (§9.2).
|
|
5
5
|
import { err, makeError, ok } from "../../kernel/index.js";
|
|
6
6
|
import { normalizeToolName } from "./contract.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function readString(record, keys) {
|
|
7
|
+
// SPEC §4.5.3: first non-empty string under any of the keys. Shared with
|
|
8
|
+
// the sibling adapters, so the field readers never drift apart.
|
|
9
|
+
export function readString(record, keys) {
|
|
11
10
|
for (const key of keys) {
|
|
12
11
|
const value = record[key];
|
|
13
12
|
if (typeof value === "string" && value.length > 0) {
|
|
@@ -16,6 +15,9 @@ function readString(record, keys) {
|
|
|
16
15
|
}
|
|
17
16
|
return undefined;
|
|
18
17
|
}
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
19
21
|
function readToolInput(raw) {
|
|
20
22
|
for (const key of ["tool_input", "toolInput", "input", "args"]) {
|
|
21
23
|
const value = raw[key];
|
|
@@ -42,34 +44,30 @@ function collectPaths(input) {
|
|
|
42
44
|
}
|
|
43
45
|
return [...found].sort();
|
|
44
46
|
}
|
|
47
|
+
// SPEC §4.5.3: event names map to triggers through one table, so adding a
|
|
48
|
+
// harness alias is a row, not a branch.
|
|
49
|
+
const CLAUDE_EVENT_TABLE = {
|
|
50
|
+
PreToolUse: "pre-tool",
|
|
51
|
+
"tool.pre.write": "pre-tool",
|
|
52
|
+
"tool.pre.shell": "pre-tool",
|
|
53
|
+
"tool.pre.read": "pre-tool",
|
|
54
|
+
"tool.pre.search": "pre-tool",
|
|
55
|
+
"tool.pre.other": "pre-tool",
|
|
56
|
+
PostToolUse: "post-tool",
|
|
57
|
+
"tool.post.write": "post-tool",
|
|
58
|
+
Stop: "post-tool",
|
|
59
|
+
stop: "post-tool",
|
|
60
|
+
SessionStart: "startup",
|
|
61
|
+
"session.start": "startup",
|
|
62
|
+
SubagentStart: "startup",
|
|
63
|
+
"subagent.start": "startup",
|
|
64
|
+
UserPromptSubmit: "prompt",
|
|
65
|
+
"prompt.submit": "prompt",
|
|
66
|
+
PostCompact: "compact",
|
|
67
|
+
"compact.post": "compact",
|
|
68
|
+
};
|
|
45
69
|
function mapClaudeEvent(event) {
|
|
46
|
-
|
|
47
|
-
case "PreToolUse":
|
|
48
|
-
case "tool.pre.write":
|
|
49
|
-
case "tool.pre.shell":
|
|
50
|
-
case "tool.pre.read":
|
|
51
|
-
case "tool.pre.search":
|
|
52
|
-
case "tool.pre.other":
|
|
53
|
-
return "pre-tool";
|
|
54
|
-
case "PostToolUse":
|
|
55
|
-
case "tool.post.write":
|
|
56
|
-
case "Stop":
|
|
57
|
-
case "stop":
|
|
58
|
-
return "post-tool";
|
|
59
|
-
case "SessionStart":
|
|
60
|
-
case "session.start":
|
|
61
|
-
case "SubagentStart":
|
|
62
|
-
case "subagent.start":
|
|
63
|
-
return "startup";
|
|
64
|
-
case "UserPromptSubmit":
|
|
65
|
-
case "prompt.submit":
|
|
66
|
-
return "prompt";
|
|
67
|
-
case "PostCompact":
|
|
68
|
-
case "compact.post":
|
|
69
|
-
return "compact";
|
|
70
|
-
default:
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
70
|
+
return CLAUDE_EVENT_TABLE[event];
|
|
73
71
|
}
|
|
74
72
|
function blockError(harness, detail) {
|
|
75
73
|
return makeError("CDX-E303", `unparseable ${harness} hook payload: ${detail}`, "Check the harness hook configuration, then retry the tool call.");
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
// SPEC §4.5.4: the GitHub Copilot adapter (CLI, cloud agent, VS Code
|
|
2
|
+
// agent). Parse, reply and staticBlock are pure; hookIoVerified stays false
|
|
3
|
+
// until a first-party capture lands (§4.5.7), and the hooks-file schema
|
|
4
|
+
// below follows the SPEC row verbatim for the same reason.
|
|
1
5
|
import { err, makeError, ok, stableStringify } from "../../kernel/index.js";
|
|
6
|
+
import { readString } from "./claude-shaped.js";
|
|
2
7
|
import { normalizeToolName } from "./contract.js";
|
|
3
8
|
import { COPILOT_CAPABILITIES } from "./copilot-capabilities.js";
|
|
4
9
|
// SPEC §4.5.4 with §4.5.5: hook entries carry timeoutSec; the runtime budget
|
|
@@ -31,40 +36,28 @@ const COPILOT_TOOL_TABLE = {
|
|
|
31
36
|
export function normalizeCopilotToolName(name) {
|
|
32
37
|
return normalizeToolName(COPILOT_TOOL_TABLE, name);
|
|
33
38
|
}
|
|
39
|
+
// SPEC §4.5.4: event names map to triggers through one table, so adding a
|
|
40
|
+
// harness alias is a row, not a branch.
|
|
41
|
+
const COPILOT_EVENT_TABLE = {
|
|
42
|
+
preToolUse: "pre-tool",
|
|
43
|
+
"tool.pre.write": "pre-tool",
|
|
44
|
+
"tool.pre.shell": "pre-tool",
|
|
45
|
+
"tool.pre.read": "pre-tool",
|
|
46
|
+
"tool.pre.search": "pre-tool",
|
|
47
|
+
"tool.pre.other": "pre-tool",
|
|
48
|
+
postToolUse: "post-tool",
|
|
49
|
+
"tool.post.write": "post-tool",
|
|
50
|
+
sessionStart: "startup",
|
|
51
|
+
"session.start": "startup",
|
|
52
|
+
userPromptSubmitted: "prompt",
|
|
53
|
+
"prompt.submit": "prompt",
|
|
54
|
+
};
|
|
34
55
|
function mapCopilotEvent(event) {
|
|
35
|
-
|
|
36
|
-
case "preToolUse":
|
|
37
|
-
case "tool.pre.write":
|
|
38
|
-
case "tool.pre.shell":
|
|
39
|
-
case "tool.pre.read":
|
|
40
|
-
case "tool.pre.search":
|
|
41
|
-
case "tool.pre.other":
|
|
42
|
-
return "pre-tool";
|
|
43
|
-
case "postToolUse":
|
|
44
|
-
case "tool.post.write":
|
|
45
|
-
return "post-tool";
|
|
46
|
-
case "sessionStart":
|
|
47
|
-
case "session.start":
|
|
48
|
-
return "startup";
|
|
49
|
-
case "userPromptSubmitted":
|
|
50
|
-
case "prompt.submit":
|
|
51
|
-
return "prompt";
|
|
52
|
-
default:
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
56
|
+
return COPILOT_EVENT_TABLE[event];
|
|
55
57
|
}
|
|
56
58
|
function isRecord(value) {
|
|
57
59
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
60
|
}
|
|
59
|
-
function readString(record, keys) {
|
|
60
|
-
for (const key of keys) {
|
|
61
|
-
const value = record[key];
|
|
62
|
-
if (typeof value === "string" && value.length > 0) {
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
61
|
function blockError(detail) {
|
|
69
62
|
return makeError("CDX-E303", `unparseable copilot hook payload: ${detail}`, "Check the harness hook configuration, then retry the tool call.");
|
|
70
63
|
}
|
|
@@ -87,18 +87,23 @@ function fitLine(event, now, id) {
|
|
|
87
87
|
return err(e801("append event", "a single event without a message exceeds 64 KB"));
|
|
88
88
|
}
|
|
89
89
|
const empty = stableLineStringify(withOptionals(base, event, ""));
|
|
90
|
-
|
|
90
|
+
const allowance = MAX_LINE_BYTES - byteLength(empty) - 2;
|
|
91
|
+
return truncateToFit(base, event, message, allowance);
|
|
92
|
+
}
|
|
93
|
+
// Halve the budget until the marked message fits, else fail closed.
|
|
94
|
+
function truncateToFit(base, event, message, allowance) {
|
|
95
|
+
let budget = allowance;
|
|
91
96
|
for (let round = 0; round < 32; round += 1) {
|
|
92
|
-
if (
|
|
97
|
+
if (budget < 0) {
|
|
93
98
|
return err(e801("append event", "the message cannot be truncated to fit 64 KB"));
|
|
94
99
|
}
|
|
95
|
-
const kept = truncateUtf8(message,
|
|
100
|
+
const kept = truncateUtf8(message, budget);
|
|
96
101
|
const omitted = byteLength(message) - byteLength(kept);
|
|
97
102
|
const candidate = stableLineStringify(withOptionals(base, event, `${kept}[truncated ${omitted} bytes]`));
|
|
98
103
|
if (byteLength(candidate) <= MAX_LINE_BYTES) {
|
|
99
104
|
return ok(candidate);
|
|
100
105
|
}
|
|
101
|
-
|
|
106
|
+
budget = Math.floor(budget / 2);
|
|
102
107
|
}
|
|
103
108
|
return err(e801("append event", "the message cannot be truncated to fit 64 KB"));
|
|
104
109
|
}
|
|
@@ -27,6 +27,20 @@ function helpersLine(helpers) {
|
|
|
27
27
|
});
|
|
28
28
|
return `helpers: ${parts.join(", ")}`;
|
|
29
29
|
}
|
|
30
|
+
// SPEC §4.18.3: trailing status rows with their fallbacks, so the screen
|
|
31
|
+
// builder stays a straight-line assembly.
|
|
32
|
+
function tailStatusLines(input) {
|
|
33
|
+
return [
|
|
34
|
+
`task: ${input.task ?? "none active"}`,
|
|
35
|
+
`waivers: ${input.waivers ?? "none in force"}`,
|
|
36
|
+
`ratchet: ${input.ratchet ?? "no baseline history"}`,
|
|
37
|
+
`deps: ${input.deps ?? "freshness unknown"}`,
|
|
38
|
+
`integrity: ${input.integrity ?? "unverified"}`,
|
|
39
|
+
`escalations: ${input.escalations ?? "none open"}`,
|
|
40
|
+
`remediation: ${input.remediation ?? "no plan"}`,
|
|
41
|
+
`events: ${input.events} observed, ${input.corrupt} corrupt${input.gap ? "; gap in coverage, log was deleted mid-session" : ""}`,
|
|
42
|
+
];
|
|
43
|
+
}
|
|
30
44
|
// SPEC §4.18.3: one screen with engine, packs, harness states, helper
|
|
31
45
|
// tools, task, waivers, ratchet, deps, integrity, escalations and
|
|
32
46
|
// remediation progress.
|
|
@@ -43,14 +57,7 @@ export function buildStatusText(input) {
|
|
|
43
57
|
lines.push(`- ${harness.harness}: ${harness.state} (${harness.layers})`);
|
|
44
58
|
}
|
|
45
59
|
lines.push(helpersLine(input.helpers));
|
|
46
|
-
lines.push(
|
|
47
|
-
lines.push(`waivers: ${input.waivers ?? "none in force"}`);
|
|
48
|
-
lines.push(`ratchet: ${input.ratchet ?? "no baseline history"}`);
|
|
49
|
-
lines.push(`deps: ${input.deps ?? "freshness unknown"}`);
|
|
50
|
-
lines.push(`integrity: ${input.integrity ?? "unverified"}`);
|
|
51
|
-
lines.push(`escalations: ${input.escalations ?? "none open"}`);
|
|
52
|
-
lines.push(`remediation: ${input.remediation ?? "no plan"}`);
|
|
53
|
-
lines.push(`events: ${input.events} observed, ${input.corrupt} corrupt${input.gap ? "; gap in coverage, log was deleted mid-session" : ""}`);
|
|
60
|
+
lines.push(...tailStatusLines(input));
|
|
54
61
|
return lines.join("\n");
|
|
55
62
|
}
|
|
56
63
|
// SPEC §4.18.3: the same status as data for the extension (--json).
|
|
@@ -13,25 +13,46 @@ function block(ruleId, message, remediation) {
|
|
|
13
13
|
}
|
|
14
14
|
// SPEC §4.15.2: reported not required → block; unreadable → advise (PRD
|
|
15
15
|
// §11.1). The same read feeds core/branch-protection below.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const findings = [];
|
|
16
|
+
function unreadableFindings() {
|
|
17
|
+
return [
|
|
18
|
+
advise("core/ci-required-check", "cannot confirm the verify check is required: branch protection is unreadable", "Grant the workflow read access to rulesets/branch protection, or confirm the required check by hand (EC-1, EC-6)."),
|
|
19
|
+
advise("core/branch-protection", "cannot confirm force-push, deletion and squash settings: branch protection is unreadable", "Grant the workflow read access to the repo settings and rulesets, or confirm them by hand (§4.15.2)."),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
function requiredCheckFinding(read) {
|
|
24
23
|
if (read.verifyRequired !== true) {
|
|
25
|
-
|
|
24
|
+
return block("core/ci-required-check", "the verify check is not a required check on the default branch", "Add verify to the required checks of the branch ruleset (§4.15 EC-7).");
|
|
26
25
|
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
function squashFinding(read) {
|
|
27
29
|
const squashExcluded = read.allowSquashMerge === false ||
|
|
28
30
|
(read.rulesetMergeMethods !== undefined &&
|
|
29
31
|
!read.rulesetMergeMethods.includes("squash"));
|
|
30
32
|
if (!squashExcluded) {
|
|
31
|
-
|
|
33
|
+
return block("core/branch-protection", "squash merging is allowed and no ruleset excludes it", "Set allow_squash_merge to false and exclude squash from the ruleset allowed_merge_methods (§4.9.3, EC-11, S33).");
|
|
32
34
|
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
function hardeningFinding(read) {
|
|
33
38
|
if (read.forcePushBlocked !== true || read.deletionBlocked !== true) {
|
|
34
|
-
|
|
39
|
+
return block("core/branch-protection", "force-push or deletion is not blocked on the default branch", "Block force-pushes and deletions on the default branch in settings and rulesets (§4.15.2).");
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
export function evaluateProtection(read) {
|
|
44
|
+
if (read === undefined || !read.readable) {
|
|
45
|
+
return unreadableFindings();
|
|
46
|
+
}
|
|
47
|
+
const findings = [];
|
|
48
|
+
for (const maybe of [
|
|
49
|
+
requiredCheckFinding(read),
|
|
50
|
+
squashFinding(read),
|
|
51
|
+
hardeningFinding(read),
|
|
52
|
+
]) {
|
|
53
|
+
if (maybe !== undefined) {
|
|
54
|
+
findings.push(maybe);
|
|
55
|
+
}
|
|
35
56
|
}
|
|
36
57
|
return findings;
|
|
37
58
|
}
|
|
@@ -51,10 +72,13 @@ export function resolveBaseRef(input) {
|
|
|
51
72
|
input.before !== zero) {
|
|
52
73
|
return ok(input.before);
|
|
53
74
|
}
|
|
75
|
+
return ok(baseFromDefault(input));
|
|
76
|
+
}
|
|
77
|
+
function baseFromDefault(input) {
|
|
54
78
|
if (input.eventName === "push") {
|
|
55
|
-
return
|
|
79
|
+
return `merge-base(${input.defaultBranch ?? "main"},HEAD)`;
|
|
56
80
|
}
|
|
57
|
-
return
|
|
81
|
+
return (input.pullBaseSha ?? `merge-base(${input.defaultBranch ?? "main"},HEAD)`);
|
|
58
82
|
}
|
|
59
83
|
function usesRef(uses) {
|
|
60
84
|
const at = uses.indexOf("@");
|
|
@@ -17,10 +17,45 @@ function stripWorkspacePrefix(range) {
|
|
|
17
17
|
}
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
|
+
function classifyWorkspaceRange(workspace) {
|
|
21
|
+
return EXACT_PIN.test(workspace) ? "workspace-exact" : "workspace-range";
|
|
22
|
+
}
|
|
23
|
+
function classifyAliasRange(range) {
|
|
24
|
+
const at = range.lastIndexOf("@");
|
|
25
|
+
const version = at < 4 ? "" : range.slice(at + 1);
|
|
26
|
+
return EXACT_PIN.test(version) ? "alias-exact" : "alias-range";
|
|
27
|
+
}
|
|
28
|
+
function isGitRange(range) {
|
|
29
|
+
return (range.startsWith("git+") ||
|
|
30
|
+
range.startsWith("git://") ||
|
|
31
|
+
range.startsWith("github:"));
|
|
32
|
+
}
|
|
33
|
+
function classifyGitRange(range) {
|
|
34
|
+
const hash = range.split("#")[1] ?? "";
|
|
35
|
+
return FULL_SHA.test(hash) ? "git-sha" : "git-unpinned";
|
|
36
|
+
}
|
|
37
|
+
function isTagName(range) {
|
|
38
|
+
return (range === "*" ||
|
|
39
|
+
range === "latest" ||
|
|
40
|
+
range === "" ||
|
|
41
|
+
/^[a-zA-Z][0-9A-Za-z.-]*$/.test(range));
|
|
42
|
+
}
|
|
43
|
+
function classifyBareRange(range) {
|
|
44
|
+
if (isGitRange(range)) {
|
|
45
|
+
return classifyGitRange(range);
|
|
46
|
+
}
|
|
47
|
+
if (range.startsWith("http://") || range.startsWith("https://")) {
|
|
48
|
+
return "tarball-url";
|
|
49
|
+
}
|
|
50
|
+
if (EXACT_PIN.test(range)) {
|
|
51
|
+
return "exact";
|
|
52
|
+
}
|
|
53
|
+
return isTagName(range) ? "tag" : "range";
|
|
54
|
+
}
|
|
20
55
|
export function classifyNpmRange(range) {
|
|
21
56
|
const workspace = stripWorkspacePrefix(range);
|
|
22
57
|
if (workspace !== undefined) {
|
|
23
|
-
return
|
|
58
|
+
return classifyWorkspaceRange(workspace);
|
|
24
59
|
}
|
|
25
60
|
if (range.startsWith("file:")) {
|
|
26
61
|
return "file";
|
|
@@ -28,30 +63,10 @@ export function classifyNpmRange(range) {
|
|
|
28
63
|
if (range.startsWith("link:")) {
|
|
29
64
|
return "link";
|
|
30
65
|
}
|
|
31
|
-
if (range.startsWith(
|
|
32
|
-
|
|
33
|
-
const version = at < 4 ? "" : range.slice(at + 1);
|
|
34
|
-
return EXACT_PIN.test(version) ? "alias-exact" : "alias-range";
|
|
35
|
-
}
|
|
36
|
-
if (range.startsWith("git+") ||
|
|
37
|
-
range.startsWith("git://") ||
|
|
38
|
-
range.startsWith("github:")) {
|
|
39
|
-
const hash = range.split("#")[1] ?? "";
|
|
40
|
-
return FULL_SHA.test(hash) ? "git-sha" : "git-unpinned";
|
|
41
|
-
}
|
|
42
|
-
if (range.startsWith("http://") || range.startsWith("https://")) {
|
|
43
|
-
return "tarball-url";
|
|
44
|
-
}
|
|
45
|
-
if (EXACT_PIN.test(range)) {
|
|
46
|
-
return "exact";
|
|
47
|
-
}
|
|
48
|
-
if (range === "*" ||
|
|
49
|
-
range === "latest" ||
|
|
50
|
-
range === "" ||
|
|
51
|
-
/^[a-zA-Z][0-9A-Za-z.-]*$/.test(range)) {
|
|
52
|
-
return "tag";
|
|
66
|
+
if (range.startsWith(`${NPM_ECOSYSTEM}:`)) {
|
|
67
|
+
return classifyAliasRange(range);
|
|
53
68
|
}
|
|
54
|
-
return
|
|
69
|
+
return classifyBareRange(range);
|
|
55
70
|
}
|
|
56
71
|
// SPEC §4.8.5: ranges, tags, *, latest and unpinned git/alias/workspace
|
|
57
72
|
// forms block core/dep-exact-pin. In-repo file:/link: forms, full-SHA git
|