@alecsibilia/luca 13.0.0-alpha.1 → 13.0.0-alpha.10

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.
Files changed (97) hide show
  1. package/dist/chunks/branch.mjs +6 -6
  2. package/dist/chunks/checks.mjs +6 -6
  3. package/dist/chunks/claim-verify.mjs +1 -1
  4. package/dist/chunks/classify.mjs +1 -1
  5. package/dist/chunks/confidence.mjs +78 -8
  6. package/dist/chunks/config-version-skew.mjs +97 -0
  7. package/dist/chunks/doctor.mjs +17 -1
  8. package/dist/chunks/hook.mjs +140 -12
  9. package/dist/chunks/init.mjs +96 -52
  10. package/dist/chunks/legacy-claude-artifacts.mjs +105 -0
  11. package/dist/chunks/legacy-package.mjs +47 -0
  12. package/dist/chunks/luca-gitignore.mjs +93 -0
  13. package/dist/chunks/muninn-mcp.mjs +78 -0
  14. package/dist/chunks/muninndb-health.mjs +1 -1
  15. package/dist/chunks/phase.mjs +28 -8
  16. package/dist/chunks/pr-review.mjs +6 -6
  17. package/dist/chunks/preferences.mjs +6 -6
  18. package/dist/chunks/repair.mjs +1 -1
  19. package/dist/chunks/repo.mjs +6 -6
  20. package/dist/chunks/retro.mjs +5 -5
  21. package/dist/chunks/roadmap.mjs +6 -6
  22. package/dist/chunks/rules.mjs +5 -5
  23. package/dist/chunks/shared-tmp-payloads.mjs +72 -0
  24. package/dist/chunks/stale-global-symlinks.mjs +78 -0
  25. package/dist/chunks/state.mjs +47 -7
  26. package/dist/chunks/stray-local-install.mjs +15 -7
  27. package/dist/chunks/telemetry.mjs +1 -1
  28. package/dist/chunks/todo.mjs +6 -6
  29. package/dist/chunks/vault-config-location.mjs +96 -0
  30. package/dist/chunks/vault-init.mjs +42 -29
  31. package/dist/chunks/verification.mjs +4 -4
  32. package/dist/chunks/workflow.mjs +6 -6
  33. package/dist/claude/.claude/agents/architect.md +44 -1
  34. package/dist/claude/.claude/agents/debater.md +89 -0
  35. package/dist/claude/.claude/agents/discussion.md +14 -26
  36. package/dist/claude/.claude/agents/execute.md +15 -3
  37. package/dist/claude/.claude/agents/executor.md +12 -23
  38. package/dist/claude/.claude/agents/finalize.md +33 -34
  39. package/dist/claude/.claude/agents/learner.md +32 -71
  40. package/dist/claude/.claude/agents/plan-reviewer.md +6 -9
  41. package/dist/claude/.claude/agents/research.md +2 -2
  42. package/dist/claude/.claude/agents/researcher.md +6 -9
  43. package/dist/claude/.claude/agents/review.md +27 -17
  44. package/dist/claude/.claude/agents/reviewer.md +19 -15
  45. package/dist/claude/.claude/agents/shadow-scanner.md +25 -27
  46. package/dist/claude/.claude/agents/test-writer.md +90 -0
  47. package/dist/claude/.claude/agents/triage.md +6 -7
  48. package/dist/claude/.claude/agents/verifier.md +6 -5
  49. package/dist/claude/.claude/commands/gh-pr-address.md +10 -10
  50. package/dist/claude/.claude/commands/lu-review.md +1 -1
  51. package/dist/claude/.claude/commands/lu.md +38 -15
  52. package/dist/claude/.claude/commands/milestone-new.md +4 -4
  53. package/dist/claude/.claude/commands/phase-execute.md +1 -1
  54. package/dist/claude/.claude/commands/phase-plan.md +1 -1
  55. package/dist/claude/.claude/commands/repo-cleanup.md +10 -7
  56. package/dist/claude/.claude/hooks/context-refresher.ts +14046 -252
  57. package/dist/claude/.claude/hooks/continuation-messages.ts +14023 -190
  58. package/dist/claude/.claude/hooks/pipeline-guard.ts +14015 -162
  59. package/dist/claude/.claude/luca-statusline.ts +146 -0
  60. package/dist/claude/skills/caveman/SKILL.md +58 -0
  61. package/dist/claude/skills/gh-issue-triage/SKILL.md +2 -2
  62. package/dist/claude/skills/gh-pr-address/SKILL.md +10 -10
  63. package/dist/claude/skills/gh-prepare/SKILL.md +1 -1
  64. package/dist/claude/skills/lu/SKILL.md +70 -19
  65. package/dist/claude/skills/lu-review/SKILL.md +1 -1
  66. package/dist/claude/skills/luca-init/SKILL.md +2 -2
  67. package/dist/claude/skills/luca-write-surface/SKILL.md +19 -3
  68. package/dist/claude/skills/milestone-audit/SKILL.md +41 -61
  69. package/dist/claude/skills/milestone-complete/SKILL.md +10 -3
  70. package/dist/claude/skills/phase-discuss/SKILL.md +16 -6
  71. package/dist/claude/skills/phase-execute/SKILL.md +92 -85
  72. package/dist/claude/skills/phase-plan/SKILL.md +27 -17
  73. package/dist/claude/skills/phase-research/SKILL.md +9 -1
  74. package/dist/claude/skills/post-init-tour/SKILL.md +1 -1
  75. package/dist/claude/skills/project-new/SKILL.md +21 -79
  76. package/dist/claude/skills/quick/SKILL.md +9 -14
  77. package/dist/claude/skills/repo-audit/SKILL.md +5 -5
  78. package/dist/claude/skills/repo-cleanup/SKILL.md +10 -7
  79. package/dist/claude/skills/session-pause/SKILL.md +1 -1
  80. package/dist/claude/skills/session-plan/SKILL.md +9 -9
  81. package/dist/claude/skills/workflow-save/SKILL.md +4 -4
  82. package/dist/index.mjs +1 -1
  83. package/dist/shared/{luca.B3saVjJm.mjs → luca.BHov6l1O.mjs} +25 -5
  84. package/dist/shared/{luca.BYdjkfnz.mjs → luca.BfjhRHhj.mjs} +1 -1
  85. package/dist/shared/{luca.DjDTeDCi.mjs → luca.BtwZ3m3l.mjs} +253 -77
  86. package/dist/shared/{luca.CRmaAfXR.mjs → luca.CI5IvdwC.mjs} +92 -27
  87. package/dist/shared/luca.ChU85KSu.mjs +32 -0
  88. package/dist/shared/{luca.BmhNkYe2.mjs → luca.DGgeB5U5.mjs} +2 -2
  89. package/dist/shared/{luca.TSMg1t7I.mjs → luca.DbrDoFFH.mjs} +1 -1
  90. package/dist/shared/{luca.CrXzXueR.mjs → luca.GSXJf0Tv.mjs} +1 -1
  91. package/dist/shared/{luca.HZxBTBgD.mjs → luca.LySXBNNL.mjs} +2 -2
  92. package/dist/shared/{luca.C4gMUoBd.mjs → luca.Xq97ytct.mjs} +3 -3
  93. package/dist/shared/luca.cizL1tYz.mjs +102 -0
  94. package/dist/shared/luca.ig-qt2eC.mjs +184 -0
  95. package/package.json +4 -4
  96. package/dist/claude/skills/autopilot/SKILL.md +0 -1299
  97. package/dist/shared/luca.B3Mimc0P.mjs +0 -52
@@ -1,6 +1,6 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import 'zod';
3
- import '../shared/luca.CRmaAfXR.mjs';
3
+ import '../shared/luca.CI5IvdwC.mjs';
4
4
  import 'node:fs';
5
5
  import 'node:fs/promises';
6
6
  import 'node:path';
@@ -8,13 +8,13 @@ import 'node:crypto';
8
8
  import 'node:module';
9
9
  import 'node:url';
10
10
  import 'node:child_process';
11
- import { r as runWriteHandler, q as lucaBranchGuardTool } from '../shared/luca.DjDTeDCi.mjs';
11
+ import { r as runWriteHandler, u as lucaBranchGuardTool } from '../shared/luca.BtwZ3m3l.mjs';
12
+ import '../shared/luca.ig-qt2eC.mjs';
12
13
  import 'node:os';
13
- import '../shared/luca.CrXzXueR.mjs';
14
- import '../shared/luca.HZxBTBgD.mjs';
15
- import '../shared/luca.TSMg1t7I.mjs';
14
+ import '../shared/luca.GSXJf0Tv.mjs';
15
+ import '../shared/luca.LySXBNNL.mjs';
16
+ import '../shared/luca.DbrDoFFH.mjs';
16
17
  import '../shared/luca.CQ3g1xrD.mjs';
17
- import '../shared/luca.B3Mimc0P.mjs';
18
18
 
19
19
  const guardCommand = defineCommand({
20
20
  meta: {
@@ -1,6 +1,6 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import 'zod';
3
- import '../shared/luca.CRmaAfXR.mjs';
3
+ import '../shared/luca.CI5IvdwC.mjs';
4
4
  import 'node:fs';
5
5
  import 'node:fs/promises';
6
6
  import 'node:path';
@@ -8,13 +8,13 @@ import 'node:crypto';
8
8
  import 'node:module';
9
9
  import 'node:url';
10
10
  import 'node:child_process';
11
- import { c as readJsonPayload, r as runWriteHandler, p as lucaChecksRunTool } from '../shared/luca.DjDTeDCi.mjs';
11
+ import { f as readJsonPayload, r as runWriteHandler, t as lucaChecksRunTool } from '../shared/luca.BtwZ3m3l.mjs';
12
+ import '../shared/luca.ig-qt2eC.mjs';
12
13
  import 'node:os';
13
- import '../shared/luca.CrXzXueR.mjs';
14
- import '../shared/luca.HZxBTBgD.mjs';
15
- import '../shared/luca.TSMg1t7I.mjs';
14
+ import '../shared/luca.GSXJf0Tv.mjs';
15
+ import '../shared/luca.LySXBNNL.mjs';
16
+ import '../shared/luca.DbrDoFFH.mjs';
16
17
  import '../shared/luca.CQ3g1xrD.mjs';
17
- import '../shared/luca.B3Mimc0P.mjs';
18
18
 
19
19
  const runCommand = defineCommand({
20
20
  meta: {
@@ -1,6 +1,6 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { resolve } from 'pathe';
3
- import '../shared/luca.CRmaAfXR.mjs';
3
+ import '../shared/luca.CI5IvdwC.mjs';
4
4
  import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
5
5
  import 'node:fs/promises';
6
6
  import { join } from 'node:path';
@@ -1,5 +1,5 @@
1
1
  import { defineCommand } from 'citty';
2
- import { C as ClassifyComplexityInputSchema } from '../shared/luca.CRmaAfXR.mjs';
2
+ import { C as ClassifyComplexityInputSchema } from '../shared/luca.CI5IvdwC.mjs';
3
3
  import 'node:fs';
4
4
  import 'node:fs/promises';
5
5
  import 'node:path';
@@ -1,6 +1,6 @@
1
1
  import { defineCommand } from 'citty';
2
- import '../shared/luca.CRmaAfXR.mjs';
3
- import { l as loadCurrentState, r as resolveActiveSlug } from '../shared/luca.CrXzXueR.mjs';
2
+ import '../shared/luca.CI5IvdwC.mjs';
3
+ import { l as loadCurrentState, r as resolveActiveSlug } from '../shared/luca.GSXJf0Tv.mjs';
4
4
  import 'node:fs';
5
5
  import 'node:fs/promises';
6
6
  import 'node:path';
@@ -8,15 +8,46 @@ import 'node:crypto';
8
8
  import 'node:module';
9
9
  import 'node:url';
10
10
  import 'node:child_process';
11
- import { b as renderConfidenceJournalMarkdown, r as readConfidenceJournal, g as getConfidenceSummary } from '../shared/luca.HZxBTBgD.mjs';
11
+ import { r as readConfidenceJournal, b as renderConfidenceJournalMarkdown, g as getConfidenceSummary } from '../shared/luca.LySXBNNL.mjs';
12
12
  import { l as logger } from '../shared/luca.dM-MKlNE.mjs';
13
13
  import 'zod';
14
- import { c as readJsonPayload, r as runWriteHandler, t as lucaConfidenceLogTool } from '../shared/luca.DjDTeDCi.mjs';
14
+ import { f as readJsonPayload, r as runWriteHandler, w as lucaConfidenceLogTool } from '../shared/luca.BtwZ3m3l.mjs';
15
+ import '../shared/luca.ig-qt2eC.mjs';
15
16
  import 'node:os';
16
- import '../shared/luca.TSMg1t7I.mjs';
17
+ import '../shared/luca.DbrDoFFH.mjs';
17
18
  import 'consola';
18
19
  import '../shared/luca.CQ3g1xrD.mjs';
19
- import '../shared/luca.B3Mimc0P.mjs';
20
+
21
+ function selectConfidenceGateActions(entries) {
22
+ const auto = [];
23
+ const research = [];
24
+ const ask = [];
25
+ for (const entry of entries) {
26
+ if (entry.resolution) {
27
+ if (entry.resolution === "auto") auto.push(entry);
28
+ else if (entry.resolution === "research") research.push(entry);
29
+ else if (entry.resolution === "ask") ask.push(entry);
30
+ else {
31
+ entry.resolution;
32
+ ask.push(entry);
33
+ }
34
+ } else if (entry.confidence === "high") {
35
+ auto.push(entry);
36
+ } else if (entry.confidence === "medium") {
37
+ auto.push(entry);
38
+ } else if (entry.confidence === "low" && entry.researchable === true) {
39
+ research.push(entry);
40
+ } else {
41
+ ask.push(entry);
42
+ }
43
+ }
44
+ return {
45
+ auto,
46
+ research,
47
+ ask,
48
+ counts: { auto: auto.length, research: research.length, ask: ask.length }
49
+ };
50
+ }
20
51
 
21
52
  async function resolveSlug(opts) {
22
53
  if (opts.explicit) return opts.explicit;
@@ -87,6 +118,14 @@ const logCommand = defineCommand({
87
118
  "review-hint": {
88
119
  type: "string",
89
120
  description: "Optional. Suggested focus area for a human reviewer."
121
+ },
122
+ researchable: {
123
+ type: "boolean",
124
+ description: "Optional. Set true when the ambiguity is factual and resolvable by automated research; absent/false means human judgment is required (gate routes to ask)."
125
+ },
126
+ resolution: {
127
+ type: "string",
128
+ description: "Optional. Explicit gate routing override: auto | research | ask. Overrides confidence-derived bucketing when set."
90
129
  }
91
130
  },
92
131
  async run({ args }) {
@@ -101,6 +140,13 @@ const logCommand = defineCommand({
101
140
  }
102
141
  payload = fromFile;
103
142
  } else {
143
+ const resolution = args.resolution;
144
+ if (resolution !== void 0 && resolution !== "auto" && resolution !== "research" && resolution !== "ask") {
145
+ logger.error(
146
+ `luca confidence log: --resolution must be one of: auto, research, ask (got "${resolution}"). Run \`luca confidence log --help\` for usage.`
147
+ );
148
+ process.exit(1);
149
+ }
104
150
  payload = {
105
151
  phase: args.phase,
106
152
  wave: args.wave !== void 0 ? Number(args.wave) : void 0,
@@ -112,7 +158,9 @@ const logCommand = defineCommand({
112
158
  reasoning: args.reasoning,
113
159
  risk: args.risk,
114
160
  files: splitCsv(args.files),
115
- ...args["review-hint"] !== void 0 ? { reviewHint: args["review-hint"] } : {}
161
+ ...args["review-hint"] !== void 0 ? { reviewHint: args["review-hint"] } : {},
162
+ ...args.researchable !== void 0 ? { researchable: args.researchable } : {},
163
+ ...resolution !== void 0 ? { resolution } : {}
116
164
  };
117
165
  }
118
166
  await runWriteHandler(
@@ -183,6 +231,27 @@ const renderCommand = defineCommand({
183
231
  `);
184
232
  }
185
233
  });
234
+ const gateCommand = defineCommand({
235
+ meta: {
236
+ name: "gate",
237
+ description: "Bucket a phase's confidence entries into gate actions and emit a JSON object shaped { auto: [...], research: [...], ask: [...], counts: { auto, research, ask } } to stdout. Each entry lands in exactly one bucket: explicit resolution overrides first, then high/medium \u2192 auto, low+researchable \u2192 research, otherwise \u2192 ask (fail-toward-human)."
238
+ },
239
+ args: {
240
+ slug: {
241
+ type: "string",
242
+ description: "Phase slug (defaults to active phase)."
243
+ }
244
+ },
245
+ async run({ args }) {
246
+ const cwd = process.cwd();
247
+ const slug = await resolveSlug({ explicit: args.slug, cwd });
248
+ const actions = selectConfidenceGateActions(
249
+ readConfidenceJournal({ cwd, slug })
250
+ );
251
+ process.stdout.write(`${JSON.stringify(actions, null, 2)}
252
+ `);
253
+ }
254
+ });
186
255
  const confidenceCommand = defineCommand({
187
256
  meta: {
188
257
  name: "confidence",
@@ -192,7 +261,8 @@ const confidenceCommand = defineCommand({
192
261
  log: logCommand,
193
262
  read: readCommand,
194
263
  summary: summaryCommand,
195
- render: renderCommand
264
+ render: renderCommand,
265
+ gate: gateCommand
196
266
  }
197
267
  });
198
268
 
@@ -0,0 +1,97 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { writeFile, readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { LUCA_VERSION } from '../index.mjs';
5
+ import 'citty';
6
+ import 'pathe';
7
+
8
+ const CHECK_NAME = "Config version skew";
9
+ function configPath() {
10
+ return join(process.cwd(), ".luca", "config.json");
11
+ }
12
+ function isDevVersion(v) {
13
+ return v === "0.0.0-dev" || v.includes("dev");
14
+ }
15
+ async function readConfig() {
16
+ const p = configPath();
17
+ if (!existsSync(p)) return null;
18
+ try {
19
+ const raw = JSON.parse(await readFile(p, "utf-8"));
20
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ const configVersionSkewCheck = {
26
+ name: CHECK_NAME,
27
+ scope: "project",
28
+ async run() {
29
+ const pass = (message) => ({
30
+ name: CHECK_NAME,
31
+ status: "pass",
32
+ message,
33
+ fixCommand: null,
34
+ details: null
35
+ });
36
+ if (isDevVersion(LUCA_VERSION)) {
37
+ return pass("dev CLI build \u2014 version skew not checked");
38
+ }
39
+ const config = await readConfig();
40
+ if (config === null) {
41
+ return pass("no .luca/config.json to check");
42
+ }
43
+ const stored = config.lucaVersion;
44
+ if (typeof stored !== "string" || stored.length === 0) {
45
+ return pass("config.json has no lucaVersion to compare");
46
+ }
47
+ if (stored === LUCA_VERSION) {
48
+ return pass(`config.lucaVersion matches CLI (${LUCA_VERSION})`);
49
+ }
50
+ return {
51
+ name: CHECK_NAME,
52
+ status: "warning",
53
+ message: `config.lucaVersion ${stored} \u2260 installed CLI ${LUCA_VERSION}`,
54
+ fixCommand: "luca doctor --fix",
55
+ details: [
56
+ `.luca/config.json records lucaVersion="${stored}" but the installed CLI is ${LUCA_VERSION}.`,
57
+ `Skills/agents that branch on lucaVersion may behave as if on the older version.`,
58
+ `Run \`luca doctor --fix\` to reconcile (rewrites lucaVersion, preserves all other config keys).`
59
+ ].join("\n ")
60
+ };
61
+ },
62
+ async fix() {
63
+ if (isDevVersion(LUCA_VERSION)) {
64
+ return { applied: [], errors: [] };
65
+ }
66
+ const config = await readConfig();
67
+ if (config === null) {
68
+ return { applied: [], errors: [] };
69
+ }
70
+ const stored = config.lucaVersion;
71
+ if (stored === LUCA_VERSION) {
72
+ return { applied: [], errors: [] };
73
+ }
74
+ try {
75
+ const next = { ...config, lucaVersion: LUCA_VERSION };
76
+ await writeFile(
77
+ configPath(),
78
+ JSON.stringify(next, null, 2) + "\n"
79
+ );
80
+ return {
81
+ applied: [
82
+ `Reconciled .luca/config.json lucaVersion ${typeof stored === "string" ? stored : "unset"} \u2192 ${LUCA_VERSION}`
83
+ ],
84
+ errors: []
85
+ };
86
+ } catch (err) {
87
+ return {
88
+ applied: [],
89
+ errors: [
90
+ `Failed to rewrite .luca/config.json lucaVersion: ${err.message}`
91
+ ]
92
+ };
93
+ }
94
+ }
95
+ };
96
+
97
+ export { configVersionSkewCheck };
@@ -7,16 +7,32 @@ async function executeDoctor(options = {}) {
7
7
  logger.info("Running environment diagnostics...\n");
8
8
  const { bunRuntimeCheck } = await import('./bun-runtime.mjs');
9
9
  const { muninndbHealthCheck } = await import('./muninndb-health.mjs');
10
+ const { muninnMcpCheck } = await import('./muninn-mcp.mjs');
10
11
  const { staleMcpServerCheck } = await import('./stale-mcp-server.mjs');
12
+ const { staleGlobalSymlinksCheck } = await import('./stale-global-symlinks.mjs');
13
+ const { legacyPackageCheck } = await import('./legacy-package.mjs');
14
+ const { legacyClaudeArtifactsCheck } = await import('./legacy-claude-artifacts.mjs');
15
+ const { sharedTmpPayloadsCheck } = await import('./shared-tmp-payloads.mjs');
11
16
  const { strayLocalInstallCheck } = await import('./stray-local-install.mjs');
17
+ const { lucaGitignoreCheck } = await import('./luca-gitignore.mjs');
18
+ const { configVersionSkewCheck } = await import('./config-version-skew.mjs');
19
+ const { vaultConfigLocationCheck } = await import('./vault-config-location.mjs');
12
20
  const allChecks = [
13
21
  // Prerequisites
14
22
  bunRuntimeCheck,
15
23
  staleMcpServerCheck,
16
24
  // Global
17
25
  muninndbHealthCheck,
26
+ muninnMcpCheck,
27
+ staleGlobalSymlinksCheck,
28
+ legacyPackageCheck,
29
+ legacyClaudeArtifactsCheck,
30
+ sharedTmpPayloadsCheck,
18
31
  // Project (cwd-dependent)
19
- strayLocalInstallCheck
32
+ strayLocalInstallCheck,
33
+ configVersionSkewCheck,
34
+ vaultConfigLocationCheck,
35
+ lucaGitignoreCheck
20
36
  ];
21
37
  const checks = scope ? allChecks.filter((check) => check.scope === scope) : allChecks;
22
38
  if (checks.length === 0) {
@@ -1,11 +1,11 @@
1
1
  import { defineCommand } from 'citty';
2
- import { c as coarsePhaseOf, a as classifyWritePath, A as AUDIT_PATH_PATTERN, W as WAVE_FILE_RE } from '../shared/luca.CRmaAfXR.mjs';
3
- import { l as loadCurrentState, r as resolveActiveSlug } from '../shared/luca.CrXzXueR.mjs';
2
+ import { c as coarsePhaseOf, t as toLucaRelative, a as classifyWritePath, b as TMP_PATH_PATTERN, A as AUDIT_PATH_PATTERN, W as WAVE_FILE_RE } from '../shared/luca.CI5IvdwC.mjs';
3
+ import { l as loadCurrentState, r as resolveActiveSlug } from '../shared/luca.GSXJf0Tv.mjs';
4
4
  import 'node:fs';
5
5
  import 'node:fs/promises';
6
6
  import 'node:path';
7
- import { S as STEP_ARTIFACTS } from '../shared/luca.B3Mimc0P.mjs';
8
- import { p as phasePathFor } from '../shared/luca.TSMg1t7I.mjs';
7
+ import { l as lucaStateClaimOwnerTool, S as STEP_ARTIFACTS } from '../shared/luca.ig-qt2eC.mjs';
8
+ import { p as phasePathFor } from '../shared/luca.DbrDoFFH.mjs';
9
9
  import 'node:crypto';
10
10
  import 'node:module';
11
11
  import 'node:url';
@@ -72,6 +72,13 @@ function isToolAllowed({
72
72
  }
73
73
 
74
74
  const READONLY_COMMANDS = /* @__PURE__ */ new Set([
75
+ // Shell navigation/no-ops: mutate shell state, not files — benign for
76
+ // the file/repo-mutation policy this classifier enforces. `cd` is the
77
+ // big one: agents prefix nearly every command with `cd <dir> && …`, so
78
+ // omitting it made every compound command classify as a mutate.
79
+ "cd",
80
+ "pushd",
81
+ "popd",
75
82
  "ls",
76
83
  "cat",
77
84
  "grep",
@@ -87,7 +94,32 @@ const READONLY_COMMANDS = /* @__PURE__ */ new Set([
87
94
  "echo",
88
95
  "printf",
89
96
  "true",
90
- "false"
97
+ "false",
98
+ // Read-only text filters. These read stdin/files and write to stdout —
99
+ // they never mutate files on their own. Omitting them made any pipeline
100
+ // through a filter (e.g. `find . | sort`) promote to bash-mutate via the
101
+ // unknown-command fallback, blocking read-only inspection in restrictive
102
+ // phases (v13 run report, M1). `tee`/`xargs` are deliberately EXCLUDED:
103
+ // `tee` writes files and `xargs` runs an arbitrary (possibly mutating)
104
+ // command, so they stay conservative.
105
+ "sort",
106
+ "uniq",
107
+ "cut",
108
+ "tr",
109
+ "comm",
110
+ "diff",
111
+ "jq",
112
+ "rg",
113
+ "column",
114
+ "nl",
115
+ "tac",
116
+ "rev",
117
+ "paste",
118
+ "fold",
119
+ "join"
120
+ // NOTE: `playwright-cli` is NOT in this set — it has a dedicated
121
+ // clause (step 4c in classifySubcommand) that extracts `--filename`
122
+ // output paths and enforces the `.playwright-cli/` artifact dir.
91
123
  ]);
92
124
  const GIT_READONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
93
125
  "status",
@@ -184,18 +216,29 @@ const PKG_MUTATE_PATTERNS = [
184
216
  ["pnpm", "add"]
185
217
  ];
186
218
  const ALWAYS_DENIED_COMMANDS = /* @__PURE__ */ new Set(["eval", "source", "."]);
219
+ const LUCA_TOPLEVEL_READ = /* @__PURE__ */ new Set(["version", "telemetry", "rules"]);
220
+ const LUCA_TOPLEVEL_WRITE = /* @__PURE__ */ new Set([
221
+ "init",
222
+ "vault:init",
223
+ "retro",
224
+ "claim-verify",
225
+ "classify",
226
+ "doctor",
227
+ "repair"
228
+ ]);
187
229
  const LUCA_READ_VERBS = /* @__PURE__ */ new Set([
188
230
  "read",
189
231
  "current",
190
232
  "list",
191
233
  "guard",
234
+ "aggregate",
192
235
  "filter-stale",
193
236
  "detect-convergence",
194
237
  "regression-check"
195
238
  ]);
196
239
  const LUCA_NOUN_VERBS = {
197
- state: /* @__PURE__ */ new Set(["read", "advance"]),
198
- phase: /* @__PURE__ */ new Set(["current"]),
240
+ state: /* @__PURE__ */ new Set(["read", "advance", "claim-owner", "set-current-phase"]),
241
+ phase: /* @__PURE__ */ new Set(["current", "advance", "archive"]),
199
242
  roadmap: /* @__PURE__ */ new Set(["read", "create"]),
200
243
  preferences: /* @__PURE__ */ new Set(["read", "write"]),
201
244
  todo: /* @__PURE__ */ new Set(["add", "list", "update"]),
@@ -208,13 +251,22 @@ const LUCA_NOUN_VERBS = {
208
251
  checks: /* @__PURE__ */ new Set(["run"]),
209
252
  branch: /* @__PURE__ */ new Set(["guard"]),
210
253
  workflow: /* @__PURE__ */ new Set(["reset"]),
211
- confidence: /* @__PURE__ */ new Set(["log"])
254
+ confidence: /* @__PURE__ */ new Set(["log"]),
255
+ // Read-side surfaces over the per-phase verify.json files.
256
+ verification: /* @__PURE__ */ new Set(["read", "aggregate"])
212
257
  };
213
258
  function classifyLucaCommand(rest) {
259
+ if (rest.some((t) => ["--help", "-h", "--version"].includes(t))) {
260
+ return "bash-readonly";
261
+ }
214
262
  const noun = rest.find((t) => !t.startsWith("-"));
215
- if (!noun) return void 0;
263
+ if (!noun) return "bash-readonly";
216
264
  const verbs = LUCA_NOUN_VERBS[noun];
217
- if (!verbs) return void 0;
265
+ if (!verbs) {
266
+ if (LUCA_TOPLEVEL_READ.has(noun)) return "bash-readonly";
267
+ if (LUCA_TOPLEVEL_WRITE.has(noun)) return "luca-write";
268
+ return void 0;
269
+ }
218
270
  const afterNoun = rest.slice(rest.indexOf(noun) + 1);
219
271
  const verb = afterNoun.find((t) => !t.startsWith("-"));
220
272
  if (!verb || !verbs.has(verb)) {
@@ -344,6 +396,20 @@ function classifySubcommand(sub) {
344
396
  return { category: lucaCategory, targetPaths: [] };
345
397
  }
346
398
  }
399
+ if (cmd === "playwright-cli") {
400
+ const target = extractFilenameFlag(rest);
401
+ if (target !== void 0) {
402
+ const inArtifactDir = target.startsWith(".playwright-cli/") && !target.split("/").includes("..");
403
+ return {
404
+ category: inArtifactDir && !sub.redirect ? "bash-readonly" : "bash-mutate",
405
+ targetPaths: [...targetsFromRedirect, target]
406
+ };
407
+ }
408
+ return {
409
+ category: sub.redirect ? "bash-mutate" : "bash-readonly",
410
+ targetPaths: targetsFromRedirect
411
+ };
412
+ }
347
413
  if (cmd === "bunx" && tokens.includes("tsc") && tokens.includes("--noEmit")) {
348
414
  return {
349
415
  category: sub.redirect ? "bash-mutate" : "bash-readonly",
@@ -358,6 +424,16 @@ function classifySubcommand(sub) {
358
424
  };
359
425
  }
360
426
  }
427
+ if (cmd === "sed" || cmd === "awk") {
428
+ const sedInPlace = cmd === "sed" && rest.some((a) => a === "--in-place" || a.startsWith("-i"));
429
+ const awkInPlace = cmd === "awk" && rest.some((a, i) => a === "-i" && rest[i + 1] === "inplace");
430
+ if (!sedInPlace && !awkInPlace) {
431
+ return {
432
+ category: sub.redirect ? "bash-mutate" : "bash-readonly",
433
+ targetPaths: targetsFromRedirect
434
+ };
435
+ }
436
+ }
361
437
  if (MUTATE_COMMANDS.has(cmd)) {
362
438
  const lastArg = cmd === "cp" || cmd === "mv" || cmd === "ln" ? lastNonFlag(rest) : void 0;
363
439
  const sedTarget = cmd === "sed" && rest.includes("-i") ? rest[rest.length - 1] : void 0;
@@ -380,6 +456,20 @@ function classifySubcommand(sub) {
380
456
  targetPaths: targetsFromRedirect
381
457
  };
382
458
  }
459
+ function extractFilenameFlag(args) {
460
+ for (let i = 0; i < args.length; i += 1) {
461
+ const a = args[i];
462
+ if (a.startsWith("--filename=")) {
463
+ const value = a.slice("--filename=".length);
464
+ return value.length > 0 ? value : void 0;
465
+ }
466
+ if (a === "--filename") {
467
+ const value = args[i + 1];
468
+ return value !== void 0 && !value.startsWith("-") ? value : void 0;
469
+ }
470
+ }
471
+ return void 0;
472
+ }
383
473
  function lastNonFlag(args) {
384
474
  for (let i = args.length - 1; i >= 0; i -= 1) {
385
475
  if (!args[i].startsWith("-")) return args[i];
@@ -476,9 +566,21 @@ async function handleStageGateHook(opts) {
476
566
  }
477
567
  const toolName = parsed.tool_name ?? parsed.toolName;
478
568
  const toolInput = parsed.tool_input ?? parsed.toolInput;
569
+ const sessionId = parsed.session_id ?? parsed.sessionId;
479
570
  const cwd = opts.cwd ?? process.cwd();
480
571
  const homedir = opts.homedir ?? process.env.HOME;
481
572
  const state = await loadCurrentState({ cwd });
573
+ if (toolName === "Bash" && sessionId && state.ownerSessionId !== sessionId && isStateAdvanceCommand(
574
+ toolInput?.command ?? ""
575
+ )) {
576
+ try {
577
+ await lucaStateClaimOwnerTool.handler({ sessionId }, { cwd });
578
+ state.ownerSessionId = sessionId;
579
+ } catch (err) {
580
+ log(`stage-gate: owner stamp skipped (${err.message})`);
581
+ }
582
+ }
583
+ const isBystander = Boolean(state.ownerSessionId) && Boolean(sessionId) && state.ownerSessionId !== sessionId;
482
584
  const phase = coarsePhaseOf(state.pipelineStep);
483
585
  if (phase === "IDLE") {
484
586
  log(
@@ -494,11 +596,12 @@ async function handleStageGateHook(opts) {
494
596
  log(`stage-gate: ${toolName} without file_path \u2014 allowing`);
495
597
  return { exitCode: 0, toolName, toolInput, decision: "allow" };
496
598
  }
497
- const pc = classifyWritePath(targetPath, { homedir });
599
+ const relTarget = toLucaRelative(targetPath, cwd);
600
+ const pc = classifyWritePath(targetPath, { homedir, cwd });
498
601
  if (pc.class === "denied") {
499
602
  pathBlockReason = `${toolName} to '${targetPath}' is always denied: ${pc.reason ?? "forbidden path"}`;
500
603
  } else if (pc.class === "planning-general" || pc.class === "planning-audit") {
501
- const gate = artifactPathGate(targetPath, state.pipelineStep, state);
604
+ const gate = artifactPathGate(relTarget, state.pipelineStep, state);
502
605
  if (gate.kind === "block") {
503
606
  const msg = `stage-gate BLOCK: ${toolName} ${gate.reason} (pipelineStep=${state.pipelineStep})`;
504
607
  log(msg);
@@ -555,6 +658,12 @@ async function handleStageGateHook(opts) {
555
658
  log("stage-gate: could not classify tool \u2014 allowing");
556
659
  return { exitCode: 0, toolName, toolInput, decision: "allow" };
557
660
  }
661
+ if (isBystander) {
662
+ log(
663
+ `stage-gate: session ${sessionId} is not the run owner (${state.ownerSessionId}) \u2014 exempting ${toolName} (category=${category}) from the phase=${phase} matrix`
664
+ );
665
+ return { exitCode: 0, toolName, toolInput, decision: "allow" };
666
+ }
558
667
  const allowed = isToolAllowed({ phase, category });
559
668
  if (!allowed) {
560
669
  const msg = `stage-gate BLOCK: ${toolName} (category=${category}) is not allowed in phase=${phase} (pipelineStep=${state.pipelineStep})`;
@@ -572,6 +681,22 @@ async function handleStageGateHook(opts) {
572
681
  );
573
682
  return { exitCode: 0, toolName, toolInput, decision: "allow" };
574
683
  }
684
+ function isStateAdvanceCommand(command) {
685
+ if (!command.includes("advance")) return false;
686
+ let entries;
687
+ try {
688
+ entries = parse(command);
689
+ } catch {
690
+ return false;
691
+ }
692
+ const toks = entries.filter((e) => typeof e === "string");
693
+ for (let i = 0; i <= toks.length - 3; i += 1) {
694
+ if (toks[i] === "luca" && toks[i + 1] === "state" && toks[i + 2] === "advance") {
695
+ return true;
696
+ }
697
+ }
698
+ return false;
699
+ }
575
700
  const FIXED_PHASE_FILE_ARTIFACTS = /* @__PURE__ */ new Set([
576
701
  "research",
577
702
  "context",
@@ -584,6 +709,9 @@ const FIXED_PHASE_FILE_ARTIFACTS = /* @__PURE__ */ new Set([
584
709
  "execute/progress"
585
710
  ]);
586
711
  function artifactPathGate(targetPath, pipelineStep, state) {
712
+ if (TMP_PATH_PATTERN.test(targetPath)) {
713
+ return { kind: "allow" };
714
+ }
587
715
  const legalArtifacts = STEP_ARTIFACTS[pipelineStep];
588
716
  if (legalArtifacts.length === 0) {
589
717
  return {