@sun-asterisk/sungen 3.3.0-alpha.1 → 3.3.0-alpha.2

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 (165) hide show
  1. package/dist/capabilities/discover.d.ts +4 -0
  2. package/dist/capabilities/discover.d.ts.map +1 -1
  3. package/dist/capabilities/discover.js +16 -2
  4. package/dist/capabilities/discover.js.map +1 -1
  5. package/dist/cli/commands/audit.js +1 -1
  6. package/dist/cli/commands/audit.js.map +1 -1
  7. package/dist/cli/commands/contract-derive-command.d.ts +3 -2
  8. package/dist/cli/commands/contract-derive-command.d.ts.map +1 -1
  9. package/dist/cli/commands/contract-derive-command.js +3 -3
  10. package/dist/cli/commands/contract-derive-command.js.map +1 -1
  11. package/dist/cli/commands/contract.js +1 -1
  12. package/dist/cli/commands/contract.js.map +1 -1
  13. package/dist/cli/commands/delivery.d.ts.map +1 -1
  14. package/dist/cli/commands/delivery.js +9 -2
  15. package/dist/cli/commands/delivery.js.map +1 -1
  16. package/dist/cli/commands/journey.js +2 -2
  17. package/dist/cli/commands/journey.js.map +1 -1
  18. package/dist/cli/commands/next.d.ts.map +1 -1
  19. package/dist/cli/commands/next.js +8 -6
  20. package/dist/cli/commands/next.js.map +1 -1
  21. package/dist/cli/index.js +1 -1
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/project-sungen-invocation.d.ts +7 -0
  24. package/dist/cli/project-sungen-invocation.d.ts.map +1 -0
  25. package/dist/cli/project-sungen-invocation.js +76 -0
  26. package/dist/cli/project-sungen-invocation.js.map +1 -0
  27. package/dist/contract/derive-free-feature.d.ts +8 -0
  28. package/dist/contract/derive-free-feature.d.ts.map +1 -1
  29. package/dist/contract/derive-free-feature.js +10 -1
  30. package/dist/contract/derive-free-feature.js.map +1 -1
  31. package/dist/contract/trace/collect-trace-evidence.js +1 -1
  32. package/dist/contract/trace/collect-trace-evidence.js.map +1 -1
  33. package/dist/contract/trace/test-step-join.d.ts +3 -3
  34. package/dist/contract/trace/test-step-join.d.ts.map +1 -1
  35. package/dist/contract/trace/test-step-join.js +2 -2
  36. package/dist/contract/trace/test-step-join.js.map +1 -1
  37. package/dist/contract/trace/trace-file-reader.d.ts +7 -1
  38. package/dist/contract/trace/trace-file-reader.d.ts.map +1 -1
  39. package/dist/contract/trace/trace-file-reader.js +10 -4
  40. package/dist/contract/trace/trace-file-reader.js.map +1 -1
  41. package/dist/contract/trace/trace-run-record.d.ts +4 -1
  42. package/dist/contract/trace/trace-run-record.d.ts.map +1 -1
  43. package/dist/contract/trace/trace-run-record.js +20 -14
  44. package/dist/contract/trace/trace-run-record.js.map +1 -1
  45. package/dist/contract/trace/trace-steps.js +1 -1
  46. package/dist/contract/trace/trace-steps.js.map +1 -1
  47. package/dist/contract/trace/trace-zip-limits.d.ts +5 -1
  48. package/dist/contract/trace/trace-zip-limits.d.ts.map +1 -1
  49. package/dist/contract/trace/trace-zip-limits.js +6 -2
  50. package/dist/contract/trace/trace-zip-limits.js.map +1 -1
  51. package/dist/decisions/confirmed-decision-commands.d.ts +11 -0
  52. package/dist/decisions/confirmed-decision-commands.d.ts.map +1 -1
  53. package/dist/decisions/confirmed-decision-commands.js +21 -3
  54. package/dist/decisions/confirmed-decision-commands.js.map +1 -1
  55. package/dist/decisions/human-confirmation.d.ts +2 -0
  56. package/dist/decisions/human-confirmation.d.ts.map +1 -1
  57. package/dist/decisions/human-confirmation.js +6 -0
  58. package/dist/decisions/human-confirmation.js.map +1 -1
  59. package/dist/exporters/matrix/xlsx-cells.js +1 -1
  60. package/dist/exporters/matrix/xlsx-cells.js.map +1 -1
  61. package/dist/graph/evaluate-graph.d.ts +3 -1
  62. package/dist/graph/evaluate-graph.d.ts.map +1 -1
  63. package/dist/graph/evaluate-graph.js +8 -5
  64. package/dist/graph/evaluate-graph.js.map +1 -1
  65. package/dist/graph/node-decl.d.ts +7 -0
  66. package/dist/graph/node-decl.d.ts.map +1 -1
  67. package/dist/graph/node-decl.js.map +1 -1
  68. package/dist/graph/nodes/test-case-contract-nodes.d.ts +3 -1
  69. package/dist/graph/nodes/test-case-contract-nodes.d.ts.map +1 -1
  70. package/dist/graph/nodes/test-case-contract-nodes.js +7 -4
  71. package/dist/graph/nodes/test-case-contract-nodes.js.map +1 -1
  72. package/dist/graph/nodes/unit-contract-node.d.ts +2 -1
  73. package/dist/graph/nodes/unit-contract-node.d.ts.map +1 -1
  74. package/dist/graph/nodes/unit-contract-node.js +8 -1
  75. package/dist/graph/nodes/unit-contract-node.js.map +1 -1
  76. package/dist/graph/nodes/unit-design-nodes.d.ts.map +1 -1
  77. package/dist/graph/nodes/unit-design-nodes.js +2 -1
  78. package/dist/graph/nodes/unit-design-nodes.js.map +1 -1
  79. package/dist/harness/catalog/gate-rules.yaml +2 -2
  80. package/dist/harness/gates/audit-report-verification.d.ts +15 -6
  81. package/dist/harness/gates/audit-report-verification.d.ts.map +1 -1
  82. package/dist/harness/gates/audit-report-verification.js +49 -20
  83. package/dist/harness/gates/audit-report-verification.js.map +1 -1
  84. package/dist/harness/gates/delivery-checklist.js +2 -2
  85. package/dist/harness/gates/delivery-checklist.js.map +1 -1
  86. package/dist/harness/gates/delivery-decision-gate.d.ts +4 -0
  87. package/dist/harness/gates/delivery-decision-gate.d.ts.map +1 -1
  88. package/dist/harness/gates/delivery-decision-gate.js +26 -1
  89. package/dist/harness/gates/delivery-decision-gate.js.map +1 -1
  90. package/dist/harness/gates/delivery-decision-step.d.ts +4 -1
  91. package/dist/harness/gates/delivery-decision-step.d.ts.map +1 -1
  92. package/dist/harness/gates/delivery-decision-step.js +17 -2
  93. package/dist/harness/gates/delivery-decision-step.js.map +1 -1
  94. package/dist/harness/journey.d.ts +19 -0
  95. package/dist/harness/journey.d.ts.map +1 -1
  96. package/dist/harness/journey.js +94 -11
  97. package/dist/harness/journey.js.map +1 -1
  98. package/dist/harness/provenance.d.ts +29 -0
  99. package/dist/harness/provenance.d.ts.map +1 -1
  100. package/dist/harness/provenance.js +109 -9
  101. package/dist/harness/provenance.js.map +1 -1
  102. package/dist/hooks/guard-protected-paths.d.ts.map +1 -1
  103. package/dist/hooks/guard-protected-paths.js +3 -2
  104. package/dist/hooks/guard-protected-paths.js.map +1 -1
  105. package/dist/hooks/guard-rules.d.ts +1 -1
  106. package/dist/hooks/guard-tty-spoof-rule.d.ts +1 -1
  107. package/dist/hooks/guard-tty-spoof-rule.js +2 -2
  108. package/dist/hooks/guard-tty-spoof-rule.js.map +1 -1
  109. package/dist/hooks/hand-back-core.d.ts +5 -3
  110. package/dist/hooks/hand-back-core.d.ts.map +1 -1
  111. package/dist/hooks/hand-back-core.js +17 -10
  112. package/dist/hooks/hand-back-core.js.map +1 -1
  113. package/dist/hooks/session-start-unit-state-hook.d.ts.map +1 -1
  114. package/dist/hooks/session-start-unit-state-hook.js +5 -3
  115. package/dist/hooks/session-start-unit-state-hook.js.map +1 -1
  116. package/dist/orchestrator/assets-drift.d.ts +5 -0
  117. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  118. package/dist/orchestrator/assets-drift.js +12 -3
  119. package/dist/orchestrator/assets-drift.js.map +1 -1
  120. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  121. package/dist/orchestrator/project-initializer.js +1 -0
  122. package/dist/orchestrator/project-initializer.js.map +1 -1
  123. package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  124. package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  125. package/package.json +3 -3
  126. package/src/capabilities/discover.ts +15 -2
  127. package/src/cli/commands/audit.ts +1 -1
  128. package/src/cli/commands/contract-derive-command.ts +7 -6
  129. package/src/cli/commands/contract.ts +1 -1
  130. package/src/cli/commands/delivery.ts +7 -2
  131. package/src/cli/commands/journey.ts +4 -4
  132. package/src/cli/commands/next.ts +8 -6
  133. package/src/cli/index.ts +1 -1
  134. package/src/cli/project-sungen-invocation.ts +39 -0
  135. package/src/contract/derive-free-feature.ts +12 -2
  136. package/src/contract/trace/collect-trace-evidence.ts +1 -1
  137. package/src/contract/trace/test-step-join.ts +6 -5
  138. package/src/contract/trace/trace-file-reader.ts +17 -7
  139. package/src/contract/trace/trace-run-record.ts +14 -8
  140. package/src/contract/trace/trace-steps.ts +1 -1
  141. package/src/contract/trace/trace-zip-limits.ts +6 -2
  142. package/src/decisions/confirmed-decision-commands.ts +25 -4
  143. package/src/decisions/human-confirmation.ts +5 -0
  144. package/src/exporters/matrix/xlsx-cells.ts +1 -1
  145. package/src/graph/evaluate-graph.ts +9 -5
  146. package/src/graph/node-decl.ts +7 -0
  147. package/src/graph/nodes/test-case-contract-nodes.ts +9 -4
  148. package/src/graph/nodes/unit-contract-node.ts +8 -1
  149. package/src/graph/nodes/unit-design-nodes.ts +2 -1
  150. package/src/harness/catalog/gate-rules.yaml +2 -2
  151. package/src/harness/gates/audit-report-verification.ts +51 -23
  152. package/src/harness/gates/delivery-checklist.ts +2 -2
  153. package/src/harness/gates/delivery-decision-gate.ts +29 -1
  154. package/src/harness/gates/delivery-decision-step.ts +19 -3
  155. package/src/harness/journey.ts +95 -13
  156. package/src/harness/provenance.ts +99 -11
  157. package/src/hooks/guard-protected-paths.ts +3 -2
  158. package/src/hooks/guard-rules.ts +1 -1
  159. package/src/hooks/guard-tty-spoof-rule.ts +3 -3
  160. package/src/hooks/hand-back-core.ts +17 -11
  161. package/src/hooks/session-start-unit-state-hook.ts +5 -3
  162. package/src/orchestrator/assets-drift.ts +12 -5
  163. package/src/orchestrator/project-initializer.ts +1 -0
  164. package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
  165. package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
@@ -2,7 +2,7 @@
2
2
  * Guard rule `tty-spoof` (phase 06, M5): ASK before an agent runs a human-confirmed sungen command
3
3
  * through a terminal emulator.
4
4
  *
5
- * `contract approve`, `journey --waive` and `journey --decide` refuse without a TTY (DOS-13.4), and
5
+ * `contract approve`, `journey --waive`, `journey --decide` and `journey --signoff` refuse without a TTY (DOS-13.4), and
6
6
  * an agent's Bash has none — unless it borrows one: `script`, `expect`, `unbuffer`, or a pty from
7
7
  * Python/Perl/zsh. Then the prompt sees a "terminal" and a piped phrase reads as a human typing it.
8
8
  * This rule spots the pair in the raw command (quotes included, so `bash -c "…"` and `expect -c '…'`
@@ -13,7 +13,7 @@
13
13
  import type { GuardRule } from './guard-rules';
14
14
 
15
15
  const TTY_EMULATOR = /\b(?:script|unbuffer|expect|pty|zpty|openpty|forkpty|ptyprocess)\b/i;
16
- const HUMAN_GATED = /\bsungen\b[\s\S]*?\b(?:contract['",\s]+approve\b|journey\b[\s\S]*?--(?:waive|decide)\b)/i;
16
+ const HUMAN_GATED = /\bsungen\b[\s\S]*?\b(?:contract['",\s]+approve\b|journey\b[\s\S]*?--(?:waive|decide|signoff)\b)/i;
17
17
 
18
18
  export const ttySpoof: GuardRule = {
19
19
  id: 'tty-spoof',
@@ -23,7 +23,7 @@ export const ttySpoof: GuardRule = {
23
23
  if (!cmd || !TTY_EMULATOR.test(cmd) || !HUMAN_GATED.test(cmd)) return null;
24
24
  return {
25
25
  status: 'ask',
26
- askReason: 'sungen: this runs a human-confirmed sungen action (`contract approve` / `journey --waive|--decide`) through a terminal '
26
+ askReason: 'sungen: this runs a human-confirmed sungen action (`contract approve` / `journey --waive|--decide|--signoff`) through a terminal '
27
27
  + 'emulator (script / expect / unbuffer / pty). Those confirmations exist so a PERSON at a terminal decides — allow only if you '
28
28
  + 'are that person and asked for exactly this; otherwise run the command yourself in your own terminal.',
29
29
  };
@@ -10,23 +10,27 @@
10
10
  * commander's preAction, so a usage error — which never reaches an action — and `main().catch`
11
11
  * ended in silence. The state now starts the moment parsing does; the action only refines it.
12
12
  */
13
+ import { sungenInvocation, withProjectInvocation } from '../cli/project-sungen-invocation';
13
14
  import { resolveUnitCandidates } from '../cli/resolve-unit';
14
15
  import { readUnitState, NextStep, UnitState } from '../harness/next-step';
15
16
  import { NO_OP, HookContext, HookInput, HookResult } from './hook-result-contract';
16
17
  import { findSungenRoot, findUnit, nearestUnit, UnitRef } from './sungen-project-root';
17
18
 
18
- /** `next` IS the answer; `hook` speaks JSON to Claude Code on stdout. Neither prints a hand-back. */
19
- const SILENT_COMMANDS = new Set(['next', 'hook']);
19
+ /** `hook` speaks JSON to Claude Code on stdout: it never prints a hand-back. */
20
+ const SILENT_COMMANDS = new Set(['hook']);
21
+ /** `next` IS the answer when it succeeds; a failed `next` answered nothing and hands back like any failure (#937). */
22
+ const ANSWER_COMMANDS = new Set(['next']);
20
23
  const UNIT_FLAG = /^(?:--screen|-s|--flow|--area|--api)$/;
21
24
  const UNIT_FLAG_EQ = /^--(?:screen|flow|area|api)=(.+)$/;
22
25
  /** A unit id is a name or `flows/<name>` — anything else is not offered back to the operator. */
23
26
  const SAFE_UNIT = /^[\w.-]+(?:\/[\w.-]+)?$/;
24
27
 
25
- export function handBackLine(unit?: string): string {
26
- return `Next: sungen next${unit ? ` ${unit}` : ''}`;
28
+ /** `invocation`: how the project runs sungen (`npx sungen` with a local install, #939). */
29
+ export function handBackLine(unit?: string, invocation = 'sungen'): string {
30
+ return `Next: ${invocation} next${unit ? ` ${unit}` : ''}`;
27
31
  }
28
32
 
29
- export interface CliHandBackState { unit?: string; machine: boolean; silent: boolean; actionRan: boolean }
33
+ export interface CliHandBackState { unit?: string; machine: boolean; silent: boolean; answer: boolean; actionRan: boolean }
30
34
 
31
35
  /** The default, set before commander parses — what a usage error or a fatal error hands back. */
32
36
  export function initialHandBackState(argv: string[]): CliHandBackState {
@@ -38,13 +42,14 @@ export function initialHandBackState(argv: string[]): CliHandBackState {
38
42
  else if (UNIT_FLAG.test(args[i]) && args[i + 1] && !args[i + 1].startsWith('-')) unit ??= args[++i];
39
43
  }
40
44
  const command = args.find((a) => !a.startsWith('-')) ?? '';
41
- return { unit, machine: args.includes('--json') || args.includes('--grep'), silent: SILENT_COMMANDS.has(command), actionRan: false };
45
+ return { unit, machine: args.includes('--json') || args.includes('--grep'), silent: SILENT_COMMANDS.has(command), answer: ANSWER_COMMANDS.has(command), actionRan: false };
42
46
  }
43
47
 
44
48
  /** commander's preAction: the action now KNOWS its unit and output mode. */
45
49
  export function onCliAction(state: CliHandBackState, name: string, opts: Record<string, unknown>, args: unknown[]): void {
46
50
  state.actionRan = true;
47
51
  if (SILENT_COMMANDS.has(name)) { state.silent = true; return; }
52
+ state.answer = ANSWER_COMMANDS.has(name);
48
53
  const flagged = [opts.screen, opts.flow, opts.area, opts.api].find((v) => typeof v === 'string' && v) as string | undefined;
49
54
  state.unit = flagged ?? (typeof args?.[0] === 'string' ? args[0] : undefined);
50
55
  // `--grep` is machine output too: callers capture it with `$(…)` (#836).
@@ -57,10 +62,11 @@ export function onCliAction(state: CliHandBackState, name: string, opts: Record<
57
62
  */
58
63
  export function exitHandBack(state: CliHandBackState, code: number, cwd: string): { stream: 'stdout' | 'stderr'; text: string } | null {
59
64
  if (state.silent) return null;
65
+ if (state.answer && code === 0) return null; // a successful `next` is the hand-back itself
60
66
  if (!state.actionRan && code === 0) return null; // --help / --version printed what was asked
61
67
  const unit = state.unit && SAFE_UNIT.test(state.unit) && !state.unit.split('/').includes('..')
62
68
  && resolveUnitCandidates(state.unit, cwd).length > 0 ? state.unit : undefined;
63
- const line = handBackLine(unit);
69
+ const line = handBackLine(unit, sungenInvocation(findSungenRoot(cwd) ?? cwd));
64
70
  if (code === 1) return { stream: 'stderr', text: `${line}\n` }; // after the error, one line
65
71
  return { stream: state.machine ? 'stderr' : 'stdout', text: `\n${line}\n` };
66
72
  }
@@ -81,10 +87,10 @@ export function attachCliHandBack(argv: string[], cwd: string): { onAction: (nam
81
87
 
82
88
  const MARK: Record<NextStep['kind'], string> = { do: '→', blocked: '⛔', optional: '○', decide: '⚖' };
83
89
 
84
- /** `sungen next` rendered compactly for an agent's context. */
85
- export function formatSteps(steps: NextStep[], max = 4): string[] {
90
+ /** `sungen next` rendered compactly for an agent's context; commands as `root` runs sungen. */
91
+ export function formatSteps(steps: NextStep[], max = 4, root?: string): string[] {
86
92
  return steps.slice(0, max).map((st) =>
87
- ` ${MARK[st.kind]} ${st.command}${st.kind === 'decide' ? ' (a QA decision — ask, do not run)' : ''}\n ${st.because}`);
93
+ ` ${MARK[st.kind]} ${root ? withProjectInvocation(st.command, root) : st.command}${st.kind === 'decide' ? ' (a QA decision — ask, do not run)' : ''}\n ${st.because}`);
88
94
  }
89
95
 
90
96
  /** The unit a session is on (a named one when it exists, else the nearest) and its derived state. */
@@ -101,6 +107,6 @@ export function run(input: HookInput, ctx: HookContext): HookResult {
101
107
  if (!found || found.state.steps.length === 0) return NO_OP;
102
108
  return {
103
109
  status: 'context',
104
- output: [`${handBackLine(found.unit.name)} — derived from what the project holds now:`, ...formatSteps(found.state.steps)].join('\n'),
110
+ output: [`${handBackLine(found.unit.name, sungenInvocation(found.root))} — derived from what the project holds now:`, ...formatSteps(found.state.steps, 4, found.root)].join('\n'),
105
111
  };
106
112
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { NO_OP, HookMeta, SungenHook } from './hook-result-contract';
11
11
  import { formatSteps, locateUnitState } from './hand-back-core';
12
+ import { sungenInvocation } from '../cli/project-sungen-invocation';
12
13
  import { listUnits } from './sungen-project-root';
13
14
  import type { UnitState } from '../harness/next-step';
14
15
 
@@ -40,14 +41,15 @@ export const sessionStartUnitStateHook: SungenHook = {
40
41
  if (!found) return NO_OP;
41
42
  const { root, unit, state } = found;
42
43
  const others = listUnits(root).length - 1;
44
+ const sungen = sungenInvocation(root);
43
45
  const lines = [
44
46
  `[sungen] Unit in progress: ${unit.name} (${unit.kind})${others > 0 ? ` — the project holds ${others} other unit(s)` : ''}.`,
45
47
  `Already done — do not redo: ${held(state)}.`,
46
48
  state.audit ? `Audit: ${state.audit.overall}/10 [${state.audit.gateStatus}] · ${state.audit.findings.length} finding(s).` : '',
47
- `Next (\`sungen next ${unit.name}\`, derived from the project state):`,
48
- ...formatSteps(state.steps),
49
+ `Next (\`${sungen} next ${unit.name}\`, derived from the project state):`,
50
+ ...formatSteps(state.steps, 4, root),
49
51
  input.source === 'compact' ? 'Context was compacted: re-read the unit\'s files before acting on them.' : '',
50
- others > 0 ? 'Working on another unit? Ask `sungen next <unit>` (or `sungen next` for all).' : '',
52
+ others > 0 ? `Working on another unit? Ask \`${sungen} next <unit>\` (or \`${sungen} next\` for all).` : '',
51
53
  ];
52
54
  return { status: 'context', output: lines.filter(Boolean).join('\n') };
53
55
  },
@@ -2,6 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { getPackageVersion } from '../exporters/package-info';
4
4
  import { readJsonConfig, warnUnreadableConfig } from '../harness/config-file';
5
+ import { updateCommand } from '../cli/project-sungen-invocation';
5
6
 
6
7
  /**
7
8
  * Are this project's AI assets (commands + skills under `.claude/` etc.) the ones
@@ -54,6 +55,16 @@ export function checkAssetsDrift(cwd: string): AssetsDrift | null {
54
55
  return { recordedVersion, runningVersion, changed, total };
55
56
  }
56
57
 
58
+ /**
59
+ * The note's text. The update command is the one this project runs (`npx sungen` with a local install)
60
+ * on the channel it runs (`--alpha` / `--beta`): a plain `sungen update` would switch to stable (#939).
61
+ */
62
+ export function assetsDriftNote(drift: AssetsDrift, cwd: string, paint: (cmd: string) => string = (c) => c): string {
63
+ return `note: this project's AI commands/skills were written by sungen ${drift.recordedVersion}, ` +
64
+ `you are running ${drift.runningVersion} — run ${paint(updateCommand(cwd, drift.runningVersion))} to refresh them ` +
65
+ '(upgrading the package alone does not).';
66
+ }
67
+
57
68
  /** One line, on stderr so `--json` output stays machine-readable. */
58
69
  export function warnAssetsDrift(cwd: string): void {
59
70
  const drift = checkAssetsDrift(cwd);
@@ -61,9 +72,5 @@ export function warnAssetsDrift(cwd: string): void {
61
72
  const gray = '\x1b[90m';
62
73
  const cyan = '\x1b[36m';
63
74
  const reset = '\x1b[0m';
64
- process.stderr.write(
65
- `${gray}note: this project's AI commands/skills were written by sungen ${drift.recordedVersion}, ` +
66
- `you are running ${drift.runningVersion} — run ${cyan}sungen update${reset}${gray} to refresh them ` +
67
- `(upgrading the package alone does not).${reset}\n`,
68
- );
75
+ process.stderr.write(`${gray}${assetsDriftNote(drift, cwd, (c) => `${cyan}${c}${reset}${gray}`)}${reset}\n`);
69
76
  }
@@ -294,6 +294,7 @@ export class ProjectInitializer {
294
294
  'specs/.auth-mobile/', // mobile auth contract = credentials (sensitive)
295
295
  '.env.appium', // local mobile-test env = machine paths + device/team ids (.example IS committed)
296
296
  'test-results/',
297
+ 'test-results-free/', // free / @impl:code runs: their own outputDir, so a compiled run cannot clear their traces (#941)
297
298
  'playwright-report/',
298
299
  '.playwright-mcp/',
299
300
  '.sungen/figma-cache/', // raw Figma node JSON (design IP)
@@ -102,8 +102,8 @@ declaration instead of leaving both on record.
102
102
 
103
103
  ## ⛔ HARD RULE — approvals and waivers are a human's, on a terminal
104
104
 
105
- `sungen contract approve`, `sungen journey --waive` and `sungen journey --decide` record a QA
106
- decision and ask for a typed confirmation on a real terminal (TTY). Never run them, never fake the
105
+ `sungen contract approve`, `sungen journey --waive`, `sungen journey --decide` and `sungen journey --signoff` record a QA
106
+ decision (the sign-off is the single human gate before delivery) and ask for a typed confirmation on a real terminal (TTY). Never run them, never fake the
107
107
  terminal (`script`, `expect`, a pty wrapper), and never edit `contracts.lock.yaml` or
108
108
  `qa/**/decisions/` by hand. When one is due, show the exact command and stop for the human.
109
109
 
@@ -268,7 +268,7 @@ question already.
268
268
  7. **Phase 3 — Full Run**: Run all tests. Fix only **new** failures (elements unique to `@normal`/`@low`). Max 1 attempt. Don't loop on low-priority failures.
269
269
  8. **Phase 4 — Regression**: One final full run. No more fix loops. Then run `sungen triage --screen <name>` and render its table as the results summary: every red or flaky scenario already sits in one class (`product-candidate`, `unclassified`, `test-defect`, `environment`, `blocked-capability`, `known-defect`, `flaky`) with the rule and one evidence line. You may move an `unclassified` row into a class only with a written evidence line of your own. You may never turn a `product-candidate` into a `test-defect` by editing the `.feature` or the expected value — the Gherkin is frozen; report it as a candidate bug. For each `product-candidate` you judge to be a product bug, run `sungen defect draft --screen <name> --scenario "<title>"`, fill the record's Summary, check its Expected against `spec.md`, then add `@known-defect:DEF-NNN` to that scenario's tag line (the tag-only exception above). The severity in the draft is `proposed` — confirm it with QA (`AskUserQuestion`) before the hand-back and set `severity_source: qa`. A row you judge is not a product bug stays in the summary with your reason, and gets no record. A `bare-timeout` row (`unclassified`) is never re-run inside this command — Phase 4 was the final run; offer the re-run in Next steps.
270
270
  8a. **Test Contract — every unit with a lock.** When `qa/<screens|flows|api>/<name>/contracts.lock.yaml` exists, check the suite still does what QA approved:
271
- - **`@free` / `@impl:code` scenarios** (hand-written specs, not compiled): in the Phase 4 final run, run their specs with `--trace on` (`npx playwright test <their spec> --trace on`) — their trace is the only proof. The project's config needs the sungen run-record reporter; if it is missing, no trace is admissible: tell the QA, do not edit the config yourself. Then `sungen contract check --screen <name> --trace`.
271
+ - **`@free` / `@impl:code` scenarios** (hand-written specs, not compiled): after the Phase 4 final run, run their specs with the project's free-test config and `--trace on` (`npx playwright test -c <free-test config> --trace on`) — their trace is the only proof. That config writes to its own `outputDir: 'test-results-free'` with `trace: 'on'` and the sungen run-record reporter; never run them into `test-results/`, which the next compiled run clears (the trace is then gone and the scenario is `unproven`). If the project has no such config, no trace is admissible: tell the QA, do not write or edit the config yourself. Then `sungen contract check --screen <name> --trace`.
272
272
  - **No free tests**: `sungen contract check --screen <name>`.
273
273
  - **Exit 2 (hard)** — `oracle-weakened`, `target-changed`, `assertion-lost`, `lock-uncommitted`, `unproven` → report each finding to the QA in the results summary, with its rule. **Never** edit the `.feature`, an expected value, the lock or a decision to turn it green, and **never** run `sungen contract approve` — approving is a QA's decision, on a terminal. **Exit 0 with `review`** → list it for the QA: review blocks `sungen delivery` until QA re-approves. **Exit 1** → report the error. No lock → skip: every scenario is `proposed`, which never blocks.
274
274
  9. **Phase 8 — Serial-cascade risk check** (#414): If any scenario has many steps, a skipped test may cause downstream steps to be incorrectly skipped due to the serial-cascade sensor detecting false "not reached" conditions. When you see "test skipped" messages during Phase 3–4 runs, check if the skipped scenario is a **blockers-exist** dependency in the test ledger (`sungen ledger query --screen <name>`). If the scenario had high step count and cascade fired, offer: *"Scenarios with deep steps can trigger serial-cascade skipping. Consider parallelizing this spec with `@concurrent` tags or breaking into smaller scenarios."* Let the user decide whether to refactor (do not auto-apply parallelization).