@amsterdamdatalabs/enact-extensions 0.1.12 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -12
- package/dist/create/enact.js +1 -1
- package/dist/create/enact.js.map +1 -1
- package/dist/create/index.d.ts +4 -3
- package/dist/create/index.d.ts.map +1 -1
- package/dist/create/index.js +9 -2
- package/dist/create/index.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +5 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +10 -3
- package/dist/install.js.map +1 -1
- package/dist/internal/agents.d.ts +6 -1
- package/dist/internal/agents.d.ts.map +1 -1
- package/dist/internal/agents.js +8 -4
- package/dist/internal/agents.js.map +1 -1
- package/dist/internal/claude.d.ts +24 -0
- package/dist/internal/claude.d.ts.map +1 -1
- package/dist/internal/claude.js +99 -0
- package/dist/internal/claude.js.map +1 -1
- package/dist/internal/platform.d.ts +3 -1
- package/dist/internal/platform.d.ts.map +1 -1
- package/dist/internal/platform.js +7 -1
- package/dist/internal/platform.js.map +1 -1
- package/dist/internal/types.d.ts +2 -1
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/principles.d.ts +28 -0
- package/dist/principles.d.ts.map +1 -0
- package/dist/principles.js +159 -0
- package/dist/principles.js.map +1 -0
- package/extensions/dev-state/.agents/plugin.json +2 -1
- package/extensions/enact-context/.agents/plugin.json +2 -1
- package/extensions/enact-context/hooks/hooks.json +0 -10
- package/extensions/enact-context/skills/enact-context/SKILL.md +14 -12
- package/extensions/enact-context/skills/enact-context/scripts/install.sh +7 -7
- package/extensions/enact-core/.agents/plugin.json +2 -1
- package/extensions/enact-core/OPERATING-PRINCIPLES.md +7 -0
- package/extensions/enact-core/hooks/hooks.json +12 -0
- package/extensions/enact-evolve/.agents/plugin.json +47 -0
- package/extensions/enact-evolve/agents/evolve-session-analyst.toml +37 -0
- package/extensions/enact-evolve/skills/session-analysis/SKILL.md +98 -0
- package/extensions/enact-evolve/skills/session-analysis/scripts/run-evolve-analysis.sh +343 -0
- package/extensions/enact-factory/.agents/plugin.json +2 -2
- package/extensions/enact-factory/agents/architect.toml +9 -5
- package/extensions/enact-factory/agents/code-reviewer.toml +9 -5
- package/extensions/enact-factory/agents/critic.toml +9 -5
- package/extensions/enact-factory/agents/executor.toml +4 -1
- package/extensions/enact-factory/agents/explore.toml +4 -1
- package/extensions/enact-factory/agents/planner.toml +4 -1
- package/extensions/enact-factory/agents/verifier.toml +9 -5
- package/extensions/enact-factory/skills/advisor/SKILL.md +82 -0
- package/extensions/enact-factory/skills/ai-slop-cleaner/SKILL.md +6 -1
- package/extensions/enact-factory/skills/autonomous-runner/SKILL.md +347 -0
- package/extensions/enact-factory/skills/azdo-ci-strategy/SKILL.md +42 -15
- package/extensions/enact-factory/skills/committee/SKILL.md +80 -0
- package/extensions/enact-factory/skills/deep-interview/SKILL.md +9 -13
- package/extensions/enact-factory/skills/drive-loop/SKILL.md +161 -31
- package/extensions/enact-factory/skills/drive-loop/references/contract-schema.md +26 -6
- package/extensions/enact-factory/skills/handoff/SKILL.md +72 -0
- package/extensions/enact-factory/skills/hyperplan/SKILL.md +11 -3
- package/extensions/enact-factory/skills/looplan/SKILL.md +34 -17
- package/extensions/enact-factory/skills/plan/SKILL.md +40 -8
- package/extensions/enact-factory/skills/remove-deadcode/SKILL.md +6 -1
- package/extensions/enact-factory/skills/research/SKILL.md +14 -4
- package/extensions/enact-factory/skills/review/SKILL.md +21 -2
- package/extensions/enact-factory/skills/security-research/SKILL.md +5 -2
- package/extensions/enact-factory/skills/tdd/SKILL.md +7 -1
- package/extensions/enact-factory/skills/testing-strategy/SKILL.md +5 -0
- package/extensions/enact-factory/skills/trace/SKILL.md +5 -0
- package/extensions/enact-factory/skills/ultraqa/SKILL.md +21 -15
- package/extensions/enact-factory/skills/work-with-workitem/SKILL.md +5 -0
- package/extensions/enact-factory/skills/workitem-triage/SKILL.md +5 -0
- package/extensions/enact-loop/.agents/plugin.json +5 -4
- package/extensions/enact-loop/scripts/validate.mjs +123 -0
- package/extensions/enact-loop/skills/enact-loop/SKILL.md +189 -30
- package/extensions/enact-wiki/.agents/plugin.json +2 -1
- package/extensions/net-revenue-management/.agents/plugin.json +2 -1
- package/extensions/plugin-dev/.agents/plugin.json +2 -1
- package/extensions/plugin-dev/skills/start/SKILL.md +3 -3
- package/package.json +1 -1
- package/scripts/check-hooks.mjs +5 -5
- package/scripts/check-principles.mjs +19 -4
- package/scripts/enact-extensions.mjs +237 -90
- package/scripts/lib/hooks.mjs +61 -217
- package/scripts/lib/migrate-artifacts.mjs +144 -0
- package/scripts/lib/principles.mjs +109 -0
- package/scripts/lib/provision-mcp.mjs +1 -1
- package/scripts/lib/run-install.mjs +72 -2
- package/scripts/lib/run-prune.mjs +23 -2
- package/scripts/lib/run-sync.mjs +4 -1
- package/scripts/postinstall.mjs +6 -6
- package/scripts/setup-enact-context.sh +20 -15
- package/scripts/version-bump.sh +22 -1
- package/spec/codex.json +5 -0
- package/spec/enact.json +3 -3
- package/spec/enact.md +1 -4
- package/spec/index.json +1 -1
- package/extensions/enact-factory/hooks/hooks.json +0 -14
- package/extensions/enact-operator/.agents/plugin.json +0 -56
- package/extensions/enact-operator/.app.json +0 -3
- package/extensions/enact-operator/.mcp.json +0 -10
- package/extensions/enact-operator/_taxonomy.md +0 -86
- package/extensions/enact-operator/agents/README.md +0 -5
- package/extensions/enact-operator/agents/architect.toml +0 -25
- package/extensions/enact-operator/agents/code-reviewer.toml +0 -24
- package/extensions/enact-operator/agents/critic.toml +0 -30
- package/extensions/enact-operator/agents/executor.toml +0 -24
- package/extensions/enact-operator/agents/explore.toml +0 -23
- package/extensions/enact-operator/agents/planner.toml +0 -24
- package/extensions/enact-operator/agents/verifier.toml +0 -24
- package/extensions/enact-operator/docs/skill-variants.md +0 -44
- package/extensions/enact-operator/hooks/hooks.json +0 -91
- package/extensions/enact-operator/skills/ai-slop-cleaner/SKILL.md +0 -50
- package/extensions/enact-operator/skills/analyze/SKILL.md +0 -91
- package/extensions/enact-operator/skills/ask/SKILL.md +0 -47
- package/extensions/enact-operator/skills/autopilot/SKILL.md +0 -170
- package/extensions/enact-operator/skills/autoresearch-goal/SKILL.md +0 -79
- package/extensions/enact-operator/skills/cancel/SKILL.md +0 -99
- package/extensions/enact-operator/skills/configure-notifications/SKILL.md +0 -77
- package/extensions/enact-operator/skills/deep-interview/SKILL.md +0 -80
- package/extensions/enact-operator/skills/doctor/SKILL.md +0 -48
- package/extensions/enact-operator/skills/hud/SKILL.md +0 -49
- package/extensions/enact-operator/skills/hyperplan/SKILL.md +0 -47
- package/extensions/enact-operator/skills/plan/SKILL.md +0 -78
- package/extensions/enact-operator/skills/ralph/SKILL.md +0 -201
- package/extensions/enact-operator/skills/ralph/gemini.md +0 -18
- package/extensions/enact-operator/skills/ralplan/SKILL.md +0 -151
- package/extensions/enact-operator/skills/remove-deadcode/SKILL.md +0 -45
- package/extensions/enact-operator/skills/research/SKILL.md +0 -74
- package/extensions/enact-operator/skills/review/SKILL.md +0 -58
- package/extensions/enact-operator/skills/security-research/SKILL.md +0 -54
- package/extensions/enact-operator/skills/setup/SKILL.md +0 -91
- package/extensions/enact-operator/skills/setup/scripts/install.sh +0 -50
- package/extensions/enact-operator/skills/skill/SKILL.md +0 -82
- package/extensions/enact-operator/skills/tdd/SKILL.md +0 -59
- package/extensions/enact-operator/skills/team/SKILL.md +0 -199
- package/extensions/enact-operator/skills/trace/SKILL.md +0 -41
- package/extensions/enact-operator/skills/ultragoal/SKILL.md +0 -99
- package/extensions/enact-operator/skills/ultraqa/SKILL.md +0 -113
- package/extensions/enact-operator/skills/ultrawork/SKILL.md +0 -145
- package/extensions/enact-operator/skills/ultrawork/planner.md +0 -28
- package/extensions/enact-operator/skills/wiki/SKILL.md +0 -41
- package/extensions/enact-operator/skills/work-with-workitem/SKILL.md +0 -51
- /package/extensions/{enact-operator → enact-evolve}/assets/icon.png +0 -0
- /package/extensions/{enact-operator → enact-evolve}/assets/logo.png +0 -0
package/scripts/lib/hooks.mjs
CHANGED
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hooks.mjs —
|
|
3
|
-
*
|
|
4
|
-
* Supports: claude, codex, cursor, enact
|
|
5
|
-
*
|
|
6
|
-
* Per-surface registration details:
|
|
7
|
-
*
|
|
8
|
-
* CLAUDE:
|
|
9
|
-
* Writes to <claudeHome>/settings.json (creates/merges JSON).
|
|
10
|
-
* Adds a `hooks.SessionStart` array entry with a command that invokes the
|
|
11
|
-
* drift-check script via `node <abs path>`. Idempotent (keyed by a stable
|
|
12
|
-
* marker string in the command). This matches the Claude Code hook format:
|
|
13
|
-
* https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
14
|
-
*
|
|
15
|
-
* CODEX / ENACT:
|
|
16
|
-
* Writes to <home>/config.toml.
|
|
17
|
-
* Adds a `[[hooks.session_start]]` entry. The codex-fork's exact TOML hook
|
|
18
|
-
* schema is approximated from the enact-agent config conventions. The entry
|
|
19
|
-
* is clearly namespaced (`[hooks]` / `[[hooks.session_start]]`) and is
|
|
20
|
-
* idempotent (guarded by a stable comment marker). If the exact schema is
|
|
21
|
-
* different in a given codex/enact build, the TOML entry is still safe to
|
|
22
|
-
* have present (unknown sections are ignored by most TOML parsers).
|
|
23
|
-
*
|
|
24
|
-
* CURSOR:
|
|
25
|
-
* Cursor uses a settings.json at <cursorHome>/settings.json, similar to
|
|
26
|
-
* Claude. Registration mirrors the claude approach (SessionStart hooks array).
|
|
27
|
-
* If Cursor's actual hook schema differs, the entry is still reversible and
|
|
28
|
-
* clearly marked.
|
|
29
|
-
*
|
|
30
|
-
* All registrations are IDEMPOTENT (safe to run twice) and REVERSIBLE
|
|
31
|
-
* (--remove cleanly undoes them).
|
|
2
|
+
* hooks.mjs — plugin hook reconciliation and stale standalone drift-hook cleanup.
|
|
32
3
|
*/
|
|
33
4
|
|
|
34
5
|
import {
|
|
@@ -38,15 +9,7 @@ import {
|
|
|
38
9
|
writeFileSync,
|
|
39
10
|
} from "node:fs";
|
|
40
11
|
import { homedir } from "node:os";
|
|
41
|
-
import { dirname, join
|
|
42
|
-
import { fileURLToPath } from "node:url";
|
|
43
|
-
|
|
44
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
45
|
-
const __dirname = dirname(__filename);
|
|
46
|
-
const REPO_ROOT = resolve(__dirname, "..", "..");
|
|
47
|
-
|
|
48
|
-
// Absolute path to the drift-check hook script.
|
|
49
|
-
const DRIFT_CHECK_SCRIPT = join(REPO_ROOT, "scripts", "hooks", "session-start-drift-check.mjs");
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
50
13
|
|
|
51
14
|
// Stable marker used to detect an existing entry (idempotency key).
|
|
52
15
|
const CLAUDE_HOOK_MARKER = "enact-extensions:session-start-drift-check";
|
|
@@ -70,30 +33,6 @@ function defaultHome(surface) {
|
|
|
70
33
|
// Claude settings.json helpers
|
|
71
34
|
// ---------------------------------------------------------------------------
|
|
72
35
|
|
|
73
|
-
/**
|
|
74
|
-
* Build the SessionStart hook entry object for Claude's settings.json.
|
|
75
|
-
* The command invokes node directly with the absolute hook script path.
|
|
76
|
-
*
|
|
77
|
-
* Claude Code hook format (from docs + live ~/.claude/settings.json):
|
|
78
|
-
* { matcher: ".*", hooks: [{ type: "command", command: "..." }] }
|
|
79
|
-
*
|
|
80
|
-
* The stable marker lives in the command string itself for idempotency
|
|
81
|
-
* detection (see CLAUDE_HOOK_MARKER / "session-start-drift-check").
|
|
82
|
-
*
|
|
83
|
-
* @returns {object} Claude hook entry in the required shape.
|
|
84
|
-
*/
|
|
85
|
-
function makeClaudeHookEntry() {
|
|
86
|
-
return {
|
|
87
|
-
matcher: ".*",
|
|
88
|
-
hooks: [
|
|
89
|
-
{
|
|
90
|
-
type: "command",
|
|
91
|
-
command: `node ${DRIFT_CHECK_SCRIPT}`,
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
36
|
/**
|
|
98
37
|
* Read and parse settings.json. Returns empty object on missing/error.
|
|
99
38
|
*/
|
|
@@ -114,37 +53,6 @@ function writeSettings(settingsPath, settings) {
|
|
|
114
53
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
115
54
|
}
|
|
116
55
|
|
|
117
|
-
/**
|
|
118
|
-
* Register the drift-check hook in Claude's (or Cursor's) settings.json.
|
|
119
|
-
* Idempotent: skips if marker already present.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} settingsPath - Absolute path to settings.json
|
|
122
|
-
* @returns {"registered"|"already_registered"} result
|
|
123
|
-
*/
|
|
124
|
-
function registerClaudeHook(settingsPath) {
|
|
125
|
-
const settings = readSettings(settingsPath);
|
|
126
|
-
if (!settings.hooks) settings.hooks = {};
|
|
127
|
-
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
128
|
-
|
|
129
|
-
// Check if already registered (idempotency).
|
|
130
|
-
// Marker is the path substring "session-start-drift-check" in the nested command.
|
|
131
|
-
const entryMatchesDriftCheck = (e) =>
|
|
132
|
-
e &&
|
|
133
|
-
(
|
|
134
|
-
// New schema: { matcher, hooks: [{ type, command }] }
|
|
135
|
-
(Array.isArray(e.hooks) && e.hooks.some((h) => (h.command ?? "").includes("session-start-drift-check"))) ||
|
|
136
|
-
// Old schema (pre-fix): { _enact_marker, command } — tolerate during transition.
|
|
137
|
-
e._enact_marker === CLAUDE_HOOK_MARKER ||
|
|
138
|
-
(e.command ?? "").includes("session-start-drift-check")
|
|
139
|
-
);
|
|
140
|
-
const alreadyPresent = settings.hooks.SessionStart.some(entryMatchesDriftCheck);
|
|
141
|
-
if (alreadyPresent) return "already_registered";
|
|
142
|
-
|
|
143
|
-
settings.hooks.SessionStart.push(makeClaudeHookEntry());
|
|
144
|
-
writeSettings(settingsPath, settings);
|
|
145
|
-
return "registered";
|
|
146
|
-
}
|
|
147
|
-
|
|
148
56
|
/**
|
|
149
57
|
* Remove the drift-check hook from Claude's (or Cursor's) settings.json.
|
|
150
58
|
* Idempotent: safe if entry not present.
|
|
@@ -185,15 +93,9 @@ function removeClaudeHook(settingsPath) {
|
|
|
185
93
|
// the entry is still safely parseable as TOML and clearly marked for removal.
|
|
186
94
|
// ---------------------------------------------------------------------------
|
|
187
95
|
|
|
188
|
-
const TOML_HOOK_BLOCK = (scriptPath) => `
|
|
189
|
-
# [enact-extensions:session-start-drift-check] BEGIN — do not edit this line
|
|
190
|
-
[[hooks.session_start]]
|
|
191
|
-
command = "node ${scriptPath}"
|
|
192
|
-
# [enact-extensions:session-start-drift-check] END
|
|
193
|
-
`;
|
|
194
|
-
|
|
195
96
|
const TOML_BEGIN_MARKER = `# [${TOML_HOOK_MARKER}] BEGIN`;
|
|
196
97
|
const TOML_END_MARKER = `# [${TOML_HOOK_MARKER}] END`;
|
|
98
|
+
const TOML_DRIFT_COMMAND_MARKER = "session-start-drift-check";
|
|
197
99
|
|
|
198
100
|
function pluginMarker(pluginName) {
|
|
199
101
|
return `enact-extensions:plugin-hooks:${pluginName}`;
|
|
@@ -281,17 +183,16 @@ function flattenHookCommands(hookConfig) {
|
|
|
281
183
|
}
|
|
282
184
|
|
|
283
185
|
function commandLooksPluginOwned(command) {
|
|
284
|
-
return /^enact-
|
|
186
|
+
return /^enact-context\s+hook\s+/.test(String(command));
|
|
285
187
|
}
|
|
286
188
|
|
|
287
189
|
/**
|
|
288
190
|
* True when `command` is owned by the named plugin's runtime CLI, i.e. it
|
|
289
|
-
* starts with `<pluginName> hook
|
|
290
|
-
* hooks as `enact-operator hook <x>` / `enact-context hook <x>`).
|
|
191
|
+
* starts with `<pluginName> hook `.
|
|
291
192
|
*
|
|
292
193
|
* Scoping removal to the SPECIFIC plugin avoids reconciling one plugin from
|
|
293
|
-
* stripping a sibling plugin's marker-less entries. Falls back to the
|
|
294
|
-
*
|
|
194
|
+
* stripping a sibling plugin's marker-less entries. Falls back to the known
|
|
195
|
+
* context hook command shape when no plugin name is given.
|
|
295
196
|
*/
|
|
296
197
|
function commandOwnedByPlugin(command, pluginName) {
|
|
297
198
|
if (!pluginName) return commandLooksPluginOwned(command);
|
|
@@ -413,7 +314,7 @@ function removeTomlPluginHooks(configPath, pluginName) {
|
|
|
413
314
|
|
|
414
315
|
// Legacy cleanup: older codex installs could preserve the hook tables while
|
|
415
316
|
// dropping the marker comments when config.toml was reserialized. Remove any
|
|
416
|
-
// remaining plain [[hooks.*]] blocks that still point at
|
|
317
|
+
// remaining plain [[hooks.*]] blocks that still point at this plugin's hook
|
|
417
318
|
// command set.
|
|
418
319
|
const lines = next.split(/\r?\n/);
|
|
419
320
|
const out = [];
|
|
@@ -473,20 +374,46 @@ function writeToml(configPath, content) {
|
|
|
473
374
|
writeFileSync(configPath, content, "utf8");
|
|
474
375
|
}
|
|
475
376
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
*
|
|
480
|
-
* @param {string} configPath
|
|
481
|
-
* @returns {"registered"|"already_registered"} result
|
|
482
|
-
*/
|
|
483
|
-
function registerTomlHook(configPath) {
|
|
484
|
-
const existing = readToml(configPath);
|
|
485
|
-
if (existing.includes(TOML_BEGIN_MARKER)) return "already_registered";
|
|
377
|
+
function stripTomlDriftHookBlocks(content) {
|
|
378
|
+
let next = content;
|
|
379
|
+
let removed = false;
|
|
486
380
|
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
381
|
+
const markedPattern = new RegExp(
|
|
382
|
+
`\\n?${TOML_BEGIN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${TOML_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]*\\n?`,
|
|
383
|
+
"g",
|
|
384
|
+
);
|
|
385
|
+
next = next.replace(markedPattern, () => {
|
|
386
|
+
removed = true;
|
|
387
|
+
return "\n";
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const lines = next.split(/\r?\n/);
|
|
391
|
+
const out = [];
|
|
392
|
+
for (let i = 0; i < lines.length;) {
|
|
393
|
+
const line = lines[i];
|
|
394
|
+
if (line.trim() === "[[hooks.session_start]]") {
|
|
395
|
+
const block = [line];
|
|
396
|
+
i += 1;
|
|
397
|
+
while (i < lines.length && !/^\s*\[/.test(lines[i])) {
|
|
398
|
+
block.push(lines[i]);
|
|
399
|
+
i += 1;
|
|
400
|
+
}
|
|
401
|
+
if (block.some((entry) => entry.includes(TOML_DRIFT_COMMAND_MARKER))) {
|
|
402
|
+
removed = true;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
out.push(...block);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
out.push(line);
|
|
409
|
+
i += 1;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
next = out.join("\n").replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "");
|
|
413
|
+
return {
|
|
414
|
+
content: next.endsWith("\n") || next.length === 0 ? next : `${next}\n`,
|
|
415
|
+
removed,
|
|
416
|
+
};
|
|
490
417
|
}
|
|
491
418
|
|
|
492
419
|
/**
|
|
@@ -498,101 +425,17 @@ function registerTomlHook(configPath) {
|
|
|
498
425
|
*/
|
|
499
426
|
function removeTomlHook(configPath) {
|
|
500
427
|
const existing = readToml(configPath);
|
|
501
|
-
|
|
428
|
+
const stripped = stripTomlDriftHookBlocks(existing);
|
|
429
|
+
if (!stripped.removed) return "not_found";
|
|
502
430
|
|
|
503
|
-
|
|
504
|
-
const beginIdx = existing.indexOf(TOML_BEGIN_MARKER);
|
|
505
|
-
const endIdx = existing.indexOf(TOML_END_MARKER);
|
|
506
|
-
if (beginIdx === -1 || endIdx === -1) return "not_found";
|
|
507
|
-
|
|
508
|
-
// Include the newline before BEGIN and after END.
|
|
509
|
-
const beforeBegin = existing.slice(0, beginIdx).replace(/\n$/, "");
|
|
510
|
-
const afterEnd = existing.slice(endIdx + TOML_END_MARKER.length).replace(/^\n/, "");
|
|
511
|
-
const newContent = beforeBegin + (afterEnd ? "\n" + afterEnd : "");
|
|
512
|
-
|
|
513
|
-
writeToml(configPath, newContent);
|
|
431
|
+
writeToml(configPath, stripped.content);
|
|
514
432
|
return "removed";
|
|
515
433
|
}
|
|
516
434
|
|
|
517
435
|
// ---------------------------------------------------------------------------
|
|
518
|
-
// Public API:
|
|
436
|
+
// Public API: removeHook / registerPluginHooks / removePluginHooks
|
|
519
437
|
// ---------------------------------------------------------------------------
|
|
520
438
|
|
|
521
|
-
/**
|
|
522
|
-
* Register the session-start drift-check hook for a given surface.
|
|
523
|
-
*
|
|
524
|
-
* @param {string} surface — "claude" | "codex" | "cursor" | "enact"
|
|
525
|
-
* @param {object} [opts]
|
|
526
|
-
* @param {string} [opts.claudeHome]
|
|
527
|
-
* @param {string} [opts.codexHome]
|
|
528
|
-
* @param {string} [opts.cursorHome]
|
|
529
|
-
* @param {string} [opts.enactHome]
|
|
530
|
-
* @param {boolean} [opts.local] — if true, use project-local home under cwd
|
|
531
|
-
* @param {string} [opts.cwd] — cwd for --local resolution
|
|
532
|
-
* @returns {{ surface: string, result: "registered"|"already_registered"|"skipped", note?: string }}
|
|
533
|
-
*/
|
|
534
|
-
export function registerHook(surface, opts = {}) {
|
|
535
|
-
try {
|
|
536
|
-
switch (surface) {
|
|
537
|
-
case "claude": {
|
|
538
|
-
const home = opts.claudeHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".claude") : defaultHome("claude"));
|
|
539
|
-
const settingsPath = join(home, "settings.json");
|
|
540
|
-
const result = registerClaudeHook(settingsPath);
|
|
541
|
-
return { surface, result, location: settingsPath };
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
case "cursor": {
|
|
545
|
-
// Cursor mirrors Claude's settings.json approach.
|
|
546
|
-
// NOTE: Cursor's actual hook schema may differ — this uses the same
|
|
547
|
-
// SessionStart format as Claude. Reverse with --remove if unwanted.
|
|
548
|
-
const home = opts.cursorHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".cursor") : defaultHome("cursor"));
|
|
549
|
-
const settingsPath = join(home, "settings.json");
|
|
550
|
-
const result = registerClaudeHook(settingsPath);
|
|
551
|
-
return {
|
|
552
|
-
surface,
|
|
553
|
-
result,
|
|
554
|
-
location: settingsPath,
|
|
555
|
-
note: "Assumption: Cursor uses settings.json SessionStart hooks (same as Claude Code). Verify if Cursor's actual hook schema differs.",
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
case "codex": {
|
|
560
|
-
const home = opts.codexHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".codex") : defaultHome("codex"));
|
|
561
|
-
const configPath = join(home, "config.toml");
|
|
562
|
-
const result = registerTomlHook(configPath);
|
|
563
|
-
return {
|
|
564
|
-
surface,
|
|
565
|
-
result,
|
|
566
|
-
location: configPath,
|
|
567
|
-
note: "Assumption: codex config.toml supports [[hooks.session_start]] with a command field.",
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
case "enact": {
|
|
572
|
-
const home = opts.enactHome ?? (opts.local ? join(opts.cwd ?? process.cwd(), ".enact", "agent") : defaultHome("enact"));
|
|
573
|
-
const configPath = join(home, "config.toml");
|
|
574
|
-
const result = registerTomlHook(configPath);
|
|
575
|
-
return {
|
|
576
|
-
surface,
|
|
577
|
-
result,
|
|
578
|
-
location: configPath,
|
|
579
|
-
note: "Assumption: enact-agent config.toml supports [[hooks.session_start]] with a command field.",
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
default:
|
|
584
|
-
return { surface, result: "skipped", note: `Unknown surface: ${surface}` };
|
|
585
|
-
}
|
|
586
|
-
} catch (err) {
|
|
587
|
-
// Never crash — return skipped with the error note.
|
|
588
|
-
return {
|
|
589
|
-
surface,
|
|
590
|
-
result: "skipped",
|
|
591
|
-
note: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
439
|
/**
|
|
597
440
|
* Remove the session-start drift-check hook for a given surface.
|
|
598
441
|
*
|
|
@@ -647,10 +490,11 @@ export function registerPluginHooks(surface, pluginName, hookConfig, opts = {})
|
|
|
647
490
|
try {
|
|
648
491
|
switch (surface) {
|
|
649
492
|
case "claude": {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
493
|
+
return {
|
|
494
|
+
surface,
|
|
495
|
+
result: "skipped",
|
|
496
|
+
note: "Claude reads plugin runtime hooks from the enabled plugin bundle. No settings.json plugin hook registration is performed.",
|
|
497
|
+
};
|
|
654
498
|
}
|
|
655
499
|
|
|
656
500
|
case "cursor": {
|
|
@@ -674,10 +518,11 @@ export function registerPluginHooks(surface, pluginName, hookConfig, opts = {})
|
|
|
674
518
|
}
|
|
675
519
|
|
|
676
520
|
case "enact": {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
521
|
+
return {
|
|
522
|
+
surface,
|
|
523
|
+
result: "skipped",
|
|
524
|
+
note: "Enact follows the Codex-compatible bundle-owned plugin hook model. No config.toml plugin hook registration is performed.",
|
|
525
|
+
};
|
|
681
526
|
}
|
|
682
527
|
|
|
683
528
|
default:
|
|
@@ -738,4 +583,3 @@ export function removePluginHooks(surface, pluginName, opts = {}) {
|
|
|
738
583
|
}
|
|
739
584
|
|
|
740
585
|
// The 4 supported surfaces.
|
|
741
|
-
export const SUPPORTED_SURFACES = ["claude", "codex", "cursor", "enact"];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent migration for global factory/loop markdown artifacts to YYMMDDHH-<slug>.md.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync, renameSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
|
|
9
|
+
const TIMESTAMPED_RE = /^(\d{8})-(.+\.md)$/;
|
|
10
|
+
const ISO_DATE_RE = /^(20\d{2})-(\d{2})-(\d{2})-(.+)$/;
|
|
11
|
+
|
|
12
|
+
export function timestampPrefixFromDate(date) {
|
|
13
|
+
const yy = String(date.getFullYear()).slice(2);
|
|
14
|
+
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
15
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
16
|
+
const hh = String(date.getHours()).padStart(2, "0");
|
|
17
|
+
return `${yy}${mm}${dd}${hh}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function targetNameForLegacy(fileName, mtimeMs, fallbackHour = "00") {
|
|
21
|
+
if (TIMESTAMPED_RE.test(fileName)) return null;
|
|
22
|
+
|
|
23
|
+
const iso = ISO_DATE_RE.exec(fileName);
|
|
24
|
+
if (iso) {
|
|
25
|
+
const [, yyyy, mm, dd, rest] = iso;
|
|
26
|
+
return `${yyyy.slice(2)}${mm}${dd}${fallbackHour}-${rest}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const mtime = new Date(mtimeMs);
|
|
30
|
+
return `${timestampPrefixFromDate(mtime)}-${fileName}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} dir
|
|
35
|
+
* @param {{ dryRun?: boolean, fallbackHour?: string }} [opts]
|
|
36
|
+
*/
|
|
37
|
+
export function migrateMarkdownDir(dir, opts = {}) {
|
|
38
|
+
const dryRun = Boolean(opts.dryRun);
|
|
39
|
+
const fallbackHour = opts.fallbackHour ?? "00";
|
|
40
|
+
const renamed = [];
|
|
41
|
+
const skipped = [];
|
|
42
|
+
const fallbacks = [];
|
|
43
|
+
|
|
44
|
+
if (!existsSync(dir)) return { renamed, skipped, fallbacks };
|
|
45
|
+
|
|
46
|
+
for (const name of readdirSync(dir)) {
|
|
47
|
+
if (!name.endsWith(".md")) continue;
|
|
48
|
+
if (TIMESTAMPED_RE.test(name)) {
|
|
49
|
+
skipped.push(name);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const target = targetNameForLegacy(name, statSync(join(dir, name)).mtimeMs, fallbackHour);
|
|
54
|
+
if (!target || target === name) {
|
|
55
|
+
skipped.push(name);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const targetPath = join(dir, target);
|
|
60
|
+
if (existsSync(targetPath)) {
|
|
61
|
+
skipped.push(name);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (ISO_DATE_RE.test(name)) {
|
|
66
|
+
fallbacks.push({ from: name, to: target, reason: "iso-date-hour-fallback" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!dryRun) {
|
|
70
|
+
renameSync(join(dir, name), targetPath);
|
|
71
|
+
}
|
|
72
|
+
renamed.push({ from: name, to: target });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { renamed, skipped, fallbacks };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {{ dryRun?: boolean, factoryRoot?: string, loopRoot?: string }} [opts]
|
|
80
|
+
*/
|
|
81
|
+
export function migrateGlobalArtifacts(opts = {}) {
|
|
82
|
+
const dryRun = Boolean(opts.dryRun);
|
|
83
|
+
const factoryRoot = opts.factoryRoot ?? join(homedir(), ".enact", "factory", "projects");
|
|
84
|
+
const loopRoot = opts.loopRoot ?? join(homedir(), ".enact", "loop", "projects");
|
|
85
|
+
const report = {
|
|
86
|
+
dryRun,
|
|
87
|
+
factory: [],
|
|
88
|
+
loop: [],
|
|
89
|
+
fallbacks: [],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (existsSync(factoryRoot)) {
|
|
93
|
+
for (const project of readdirSync(factoryRoot, { withFileTypes: true })) {
|
|
94
|
+
if (!project.isDirectory()) continue;
|
|
95
|
+
const plansDir = join(factoryRoot, project.name, "plans");
|
|
96
|
+
const researchDir = join(factoryRoot, project.name, "research");
|
|
97
|
+
for (const dir of [plansDir, researchDir]) {
|
|
98
|
+
const result = migrateMarkdownDir(dir, { dryRun });
|
|
99
|
+
if (result.renamed.length || result.skipped.length) {
|
|
100
|
+
report.factory.push({ project: project.name, dir, ...result });
|
|
101
|
+
}
|
|
102
|
+
report.fallbacks.push(...result.fallbacks.map((f) => ({ ...f, project: project.name, dir })));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (existsSync(loopRoot)) {
|
|
108
|
+
for (const project of readdirSync(loopRoot, { withFileTypes: true })) {
|
|
109
|
+
if (!project.isDirectory()) continue;
|
|
110
|
+
const stateDir = join(loopRoot, project.name, "state");
|
|
111
|
+
const legacy = join(stateDir, "summary.md");
|
|
112
|
+
if (!existsSync(legacy)) continue;
|
|
113
|
+
|
|
114
|
+
const hasTimestamped = readdirSync(stateDir).some((name) =>
|
|
115
|
+
/^\d{8}-loop-summary\.md$/.test(name),
|
|
116
|
+
);
|
|
117
|
+
if (hasTimestamped) {
|
|
118
|
+
report.loop.push({
|
|
119
|
+
project: project.name,
|
|
120
|
+
dir: stateDir,
|
|
121
|
+
renamed: [],
|
|
122
|
+
skipped: ["summary.md"],
|
|
123
|
+
fallbacks: [],
|
|
124
|
+
});
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const mtime = statSync(legacy).mtimeMs;
|
|
129
|
+
const target = `${timestampPrefixFromDate(new Date(mtime))}-loop-summary.md`;
|
|
130
|
+
if (!dryRun) {
|
|
131
|
+
renameSync(legacy, join(stateDir, target));
|
|
132
|
+
}
|
|
133
|
+
report.loop.push({
|
|
134
|
+
project: project.name,
|
|
135
|
+
dir: stateDir,
|
|
136
|
+
renamed: [{ from: "summary.md", to: target }],
|
|
137
|
+
skipped: [],
|
|
138
|
+
fallbacks: [],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return report;
|
|
144
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* principles.mjs — Operating-principles block injection for host agent files.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces: claude, codex, cursor, enact
|
|
5
|
+
* Writes managed blocks into CLAUDE.md / AGENTS.md (not settings.json).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
applyBlock,
|
|
10
|
+
checkBlock,
|
|
11
|
+
removeBlock,
|
|
12
|
+
renderBlock,
|
|
13
|
+
resolveHostFile,
|
|
14
|
+
SUPPORTED_PRINCIPLES_SURFACES,
|
|
15
|
+
} from "../../dist/index.js";
|
|
16
|
+
|
|
17
|
+
export { SUPPORTED_PRINCIPLES_SURFACES };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} surface
|
|
21
|
+
* @param {object} [opts]
|
|
22
|
+
* @returns {{ surface: string, path: string, result: string, action?: string }}
|
|
23
|
+
*/
|
|
24
|
+
export function setupPrinciples(surface, opts = {}) {
|
|
25
|
+
const hostFile = resolveHostFile(surface, opts);
|
|
26
|
+
const block = renderBlock(opts.doctrinePath);
|
|
27
|
+
const { action } = applyBlock(hostFile, block);
|
|
28
|
+
return {
|
|
29
|
+
surface,
|
|
30
|
+
path: hostFile,
|
|
31
|
+
result: action === "noop" ? "already_present" : "applied",
|
|
32
|
+
action,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} surface
|
|
38
|
+
* @param {object} [opts]
|
|
39
|
+
* @returns {{ surface: string, path: string, result: string }}
|
|
40
|
+
*/
|
|
41
|
+
export function removePrinciples(surface, opts = {}) {
|
|
42
|
+
const hostFile = resolveHostFile(surface, opts);
|
|
43
|
+
const { removed } = removeBlock(hostFile);
|
|
44
|
+
return {
|
|
45
|
+
surface,
|
|
46
|
+
path: hostFile,
|
|
47
|
+
result: removed ? "removed" : "not_found",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} surface
|
|
53
|
+
* @param {object} [opts]
|
|
54
|
+
* @returns {{ surface: string, path: string, status: string }}
|
|
55
|
+
*/
|
|
56
|
+
export function checkPrinciples(surface, opts = {}) {
|
|
57
|
+
const hostFile = resolveHostFile(surface, opts);
|
|
58
|
+
const block = renderBlock(opts.doctrinePath);
|
|
59
|
+
const status = checkBlock(hostFile, block);
|
|
60
|
+
return { surface, path: hostFile, status };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} surface
|
|
65
|
+
* @param {object} [opts]
|
|
66
|
+
* @returns {{ surface: string, path: string, status: string, action?: string, result?: string }}
|
|
67
|
+
*/
|
|
68
|
+
export function fixPrinciples(surface, opts = {}) {
|
|
69
|
+
const check = checkPrinciples(surface, opts);
|
|
70
|
+
if (check.status === "present") {
|
|
71
|
+
return { ...check, result: "already_present", action: "noop" };
|
|
72
|
+
}
|
|
73
|
+
const setup = setupPrinciples(surface, opts);
|
|
74
|
+
const after = checkPrinciples(surface, opts);
|
|
75
|
+
return {
|
|
76
|
+
surface,
|
|
77
|
+
path: setup.path,
|
|
78
|
+
status: after.status,
|
|
79
|
+
action: setup.action,
|
|
80
|
+
result: setup.result,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string[]} surfaces
|
|
86
|
+
* @param {object} [opts]
|
|
87
|
+
* @param {boolean} [opts.fix]
|
|
88
|
+
* @returns {{ rows: object[], drift: boolean, exitCode: number }}
|
|
89
|
+
*/
|
|
90
|
+
export function runDoctor(surfaces, opts = {}) {
|
|
91
|
+
const rows = [];
|
|
92
|
+
let exitCode = 0;
|
|
93
|
+
|
|
94
|
+
for (const surface of surfaces) {
|
|
95
|
+
try {
|
|
96
|
+
const row = opts.fix ? fixPrinciples(surface, opts) : checkPrinciples(surface, opts);
|
|
97
|
+
rows.push(row);
|
|
98
|
+
if (row.status !== "present") {
|
|
99
|
+
exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
103
|
+
rows.push({ surface, path: "", status: "error", error: message });
|
|
104
|
+
exitCode = 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { rows, drift: exitCode !== 0, exitCode };
|
|
109
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The script-layer helpers remain for backwards compatibility with tests and
|
|
5
5
|
* the CLI wrapper, but the source of truth now lives in src/provision.ts and is
|
|
6
|
-
* exported from dist/index.js for in-process consumers
|
|
6
|
+
* exported from dist/index.js for in-process consumers.
|
|
7
7
|
*/
|
|
8
8
|
import {
|
|
9
9
|
provisionMcp as provisionMcpCore,
|