@alexandrealvaro/agentic 0.2.0-beta.1 → 0.3.0-beta.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.
package/README.md CHANGED
@@ -43,6 +43,42 @@ A short TUI shows the detected mode, agent, and feature signals (frontend / `.cl
43
43
 
44
44
  If your project already has an `AGENTS.md` (or `CLAUDE.md`), the installer appends a managed `Skills installed by agentic` section bracketed by `<!-- agentic-managed-skills:start -->` / `:end -->` markers. User content outside those markers is byte-preserved; re-runs update only the managed block.
45
45
 
46
+ ## Updating an existing project
47
+
48
+ To pull upstream kit changes into a project that already has agentic skills installed:
49
+
50
+ ```bash
51
+ cd your-project
52
+ npx @alexandrealvaro/agentic@beta update
53
+ ```
54
+
55
+ `update` is a separate command from `init` (clearer intent) and runs a three-way diff against a state file the kit writes at install time:
56
+
57
+ * `.claude/agentic-state.json` — for Claude Code installs.
58
+ * `.agents/agentic-state.json` — for Codex installs.
59
+
60
+ These state files are committed to your repo so the whole team shares one view of what skill version is in place. They record kit version, per-skill version, and the SHA of every shipped file at the time of last install. The three-way diff uses those SHAs to distinguish *user-edited* files from *kit-changed* files and acts accordingly:
61
+
62
+ | File state | Action |
63
+ | --- | --- |
64
+ | New file in the kit | install |
65
+ | Kit unchanged, you didn't touch it | report unchanged |
66
+ | Kit unchanged, you edited it | keep your edits |
67
+ | Kit changed, you didn't touch it | silent update |
68
+ | Kit changed, you also edited it | prompt with diff (default: skip) |
69
+ | Skill removed from the kit or de-selected | prompt before removing your file (default: keep) |
70
+
71
+ Useful flags:
72
+
73
+ * `--dry-run` — print the action plan without writing anything. Always start here when you're not sure what will happen.
74
+ * `--force` — overwrite user-edited files on conflict (non-interactive default: no). Escape hatch when you genuinely want kit-side content to win.
75
+ * `--agent claude-code | codex | both` — restrict the update to one agent.
76
+ * `--yes` — non-interactive, accepts defaults (skip on conflict, keep orphans). Combine with `--force` if you want overwrites in CI.
77
+
78
+ If the project was installed with a kit version older than v0.3 (no state file present), the first `update` falls back to today's byte-compare behavior, then writes the state file so subsequent runs use the three-way diff.
79
+
80
+ The `agentic-review` skill writes the assembled WORKFLOW §10 handoff to `.agentic/reviews/<ISO-timestamp>-<scope>.md` before delegating to the fresh-context reviewer (Claude Code) or before instructing you to `/clear` and paste (Codex). These files are ephemeral audit artifacts — add `.agentic/reviews/` to your `.gitignore`.
81
+
46
82
  For persistent install:
47
83
 
48
84
  ```bash
@@ -100,10 +136,14 @@ your-project/
100
136
  │ │ └── NNNN-<title>.md
101
137
  │ └── tasks/
102
138
  │ └── NNNN-<slug>.md
139
+ ├── .agentic/
140
+ │ └── reviews/ (gitignored — ephemeral §10 review handoffs)
103
141
  ├── .claude/ (Claude Code targets)
142
+ │ ├── agentic-state.json (kit install state — committed)
104
143
  │ ├── skills/agentic-*/SKILL.md
105
144
  │ └── agents/fresh-context-reviewer.md
106
145
  └── .agents/ (Codex targets, cc-sdd convention)
146
+ ├── agentic-state.json (kit install state — committed)
107
147
  └── skills/agentic-*/{SKILL.md, agents/openai.yaml}
108
148
  ```
109
149
 
package/WORKFLOW.md CHANGED
@@ -49,7 +49,20 @@ Avoid putting implementation code in docs unless it's executable, generated, or
49
49
 
50
50
  The split is simple. **Docs are for the *why*** — decisions, not history. Git tracks history; docs explain the reasoning that won't survive otherwise. **Code is for the *what*** — clean naming and small units make logic self-evident, and the more your code does this work, the less your docs need to.
51
51
 
52
- Comments are exceptions. They justify *why* a non-obvious choice was made — never *what* the line does. No commented-out code, and no orphan `TODO` or `FIXME`: every deferred item references an issue, ADR, or explicit follow-up.
52
+ Comments are exceptions. They justify *why* a non-obvious choice was made — never *what* the line does. No commented-out code, and no orphan `TODO` or `FIXME`: every deferred item references a tracked work item — a GitHub Issue or a per-task file under `doc/tasks/NNNN-*.md`.
53
+
54
+ ### Documentation Discipline
55
+
56
+ Eight rules apply to every document the agent or the human writes in the project. They are framed against the failure modes the kit has actually seen — bloated `AGENTS.md`, README pages that drift into changelogs, decision artifacts diluted by speculation.
57
+
58
+ 1. **Definitions and decisions only.** Documentation captures what is true now and the decisions that brought it there. Speculation, history, and unfounded plans are out. A deferred decision is in scope when it is *recorded* — an accepted ADR or a task file is fundamentação; "we might do X later" without a record is speculation and is cut.
59
+ 2. **No dates, version stamps, `DRAFT` markers, or changelogs in narrative documents.** Applies to `README.md`, `AGENTS.md` / `CLAUDE.md`, `ARCHITECTURE.md`, `DESIGN.md`, specs, and any prose page. **Decision-record artifacts are exempt** — ADRs under `doc/adr/` (Nygard `Status` lifecycle requires a date) and tasks under `doc/tasks/` (append-only `Notes` log is the auditability surface). Use git history for narrative-doc evolution; keep the dated lifecycle inside the artifacts that need it.
60
+ 3. **No emoji anywhere.** Not in docs, code, source comments, commit messages, PR bodies, or skill outputs. Severity tags use plain words (`Blocker / Concern / Note`); status uses words (`accepted / superseded`); structural cues use Markdown.
61
+ 4. **Business context first.** Every document opens with *why* — the problem, the constraint, the user — before *what* and *how*. The first paragraph answers "what would break if this document didn't exist".
62
+ 5. **One scope per document. No duplication.** If two documents would say the same thing, link instead of copying. The canonical location owns the content; everywhere else references it.
63
+ 6. **Code is the primary documentation of behavior.** Comments justify *why* a non-obvious choice was made — never restate *what* the line does. If the comment is needed to explain *what*, rename or refactor.
64
+ 7. **No commented-out code; no orphan `TODO` / `FIXME` in source.** Every deferred item references a tracked work item — a GitHub Issue, or a per-task file under `doc/tasks/NNNN-*.md` (the kit's file-based tracking surface). The trace must be addressable from the source line.
65
+ 8. **Tests are living documentation of behavior.** Test names and assertions should read as the spec they enforce. When the spec changes, the test changes; when the test changes, the spec it documents must already have changed.
53
66
 
54
67
  ## 3. Format by Evidence
55
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexandrealvaro/agentic",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.3.0-beta.2",
4
4
  "description": "Bootstrap and audit AGENTS.md, ARCHITECTURE.md, ADRs, skills, and subagents for engineering production code with LLMs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "scripts": {
22
22
  "start": "node bin/agentic.js",
23
- "test": "node bin/agentic.js --help && node bin/agentic.js init --help && node --test test/*.test.js",
23
+ "test": "node bin/agentic.js --help && node bin/agentic.js init --help && node bin/agentic.js update --help && node --test test/*.test.js",
24
24
  "prepublishOnly": "npm test"
25
25
  },
26
26
  "keywords": [
@@ -1,8 +1,17 @@
1
1
  import * as p from '@clack/prompts';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
2
5
  import { detectAgents, detectFeatures, detectMode } from '../lib/detect.js';
3
6
  import { installSkills } from '../lib/install.js';
7
+ import { saveState, loadState } from '../lib/state.js';
4
8
  import { updateRootDoc } from '../lib/rootdoc.js';
5
9
 
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const pkg = JSON.parse(
12
+ readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')
13
+ );
14
+
6
15
  const VALID_AGENTS = ['claude-code', 'codex'];
7
16
  const AGENT_FLAG_VALUES = ['claude-code', 'codex', 'both'];
8
17
 
@@ -20,7 +29,9 @@ const AGENT_LABEL = {
20
29
  const ACTION_SYMBOL = {
21
30
  created: '+',
22
31
  replaced: '~',
32
+ updated: '~',
23
33
  unchanged: '·',
34
+ kept: '·',
24
35
  skipped: '!',
25
36
  };
26
37
 
@@ -206,13 +217,17 @@ export async function initCommand(opts) {
206
217
  for (const agent of agents) {
207
218
  const agentSkills = skillsForAgent(agent, optedSkills);
208
219
  for (const s of agentSkills) installedSkillSet.add(s);
209
- const { actions } = await installSkills({
220
+ const previousStates = { [agent]: loadState(cwd, agent) };
221
+ const { actions, nextStates } = await installSkills({
210
222
  cwd,
211
223
  agents: [agent],
212
224
  skills: agentSkills,
213
225
  confirmReplace,
226
+ previousStates,
227
+ kitVersion: pkg.version,
214
228
  });
215
229
  allActions.push(...actions);
230
+ saveState(cwd, agent, nextStates[agent]);
216
231
  }
217
232
 
218
233
  const skillDisplayOrder = [
@@ -0,0 +1,251 @@
1
+ import * as p from '@clack/prompts';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ import { detectAgents, detectFeatures } from '../lib/detect.js';
6
+ import { installSkills, removeOrphanSkills } from '../lib/install.js';
7
+ import { loadState, saveState } from '../lib/state.js';
8
+ import { updateRootDoc } from '../lib/rootdoc.js';
9
+ import { CONDITIONAL_SKILLS, REQUIRED_SKILLS } from './init.js';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const pkg = JSON.parse(
13
+ readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')
14
+ );
15
+
16
+ const VALID_AGENTS = ['claude-code', 'codex'];
17
+ const AGENT_FLAG_VALUES = ['claude-code', 'codex', 'both'];
18
+
19
+ const AGENT_LABEL = {
20
+ 'claude-code': 'Claude Code',
21
+ codex: 'Codex',
22
+ };
23
+
24
+ const ACTION_SYMBOL = {
25
+ created: '+',
26
+ updated: '~',
27
+ replaced: '~',
28
+ unchanged: '·',
29
+ kept: '·',
30
+ skipped: '!',
31
+ removed: '-',
32
+ 'orphan-kept': '?',
33
+ };
34
+
35
+ const ROOT_DOC_LABEL = {
36
+ appended: '+ ',
37
+ updated: '~ ',
38
+ unchanged: '· ',
39
+ skipped: '! ',
40
+ absent: '',
41
+ };
42
+
43
+ function resolveAgents(flagValue, detectedAgents, previousAgents) {
44
+ if (flagValue === 'both') return ['claude-code', 'codex'];
45
+ if (flagValue) return [flagValue];
46
+ if (previousAgents.length > 0) return previousAgents;
47
+ if (detectedAgents.length > 0) return detectedAgents;
48
+ return ['claude-code'];
49
+ }
50
+
51
+ function pickConditionalAuto(features, targetAgents) {
52
+ return CONDITIONAL_SKILLS.filter(
53
+ (s) => s.autoIf(features) && s.agents.some((a) => targetAgents.includes(a))
54
+ ).map((s) => s.name);
55
+ }
56
+
57
+ function skillsForAgent(agent, optedSkills) {
58
+ const conditional = optedSkills.filter((skillName) => {
59
+ const def = CONDITIONAL_SKILLS.find((s) => s.name === skillName);
60
+ return def && def.agents.includes(agent);
61
+ });
62
+ return [...REQUIRED_SKILLS, ...conditional];
63
+ }
64
+
65
+ function previousAgentsFromStates(cwd) {
66
+ const out = [];
67
+ for (const agent of VALID_AGENTS) {
68
+ const state = loadState(cwd, agent);
69
+ if (state) out.push(agent);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ function previouslyOptedConditional(previousStates, currentAgents) {
75
+ const opted = new Set();
76
+ for (const agent of currentAgents) {
77
+ const prev = previousStates[agent];
78
+ if (!prev) continue;
79
+ for (const skill of Object.keys(prev.skills ?? {})) {
80
+ if (CONDITIONAL_SKILLS.some((s) => s.name === skill)) {
81
+ opted.add(skill);
82
+ }
83
+ }
84
+ }
85
+ return [...opted];
86
+ }
87
+
88
+ export async function updateCommand(opts) {
89
+ if (opts.agent && !AGENT_FLAG_VALUES.includes(opts.agent)) {
90
+ throw new Error(
91
+ `invalid agent "${opts.agent}". Use one of: ${AGENT_FLAG_VALUES.join(', ')}`
92
+ );
93
+ }
94
+
95
+ const cwd = process.cwd();
96
+ const interactive = process.stdout.isTTY && !opts.yes && !opts.agent;
97
+ const dryRun = Boolean(opts.dryRun);
98
+ const force = Boolean(opts.force);
99
+
100
+ const detectedAgents = detectAgents(cwd);
101
+ const features = detectFeatures(cwd);
102
+ const previousAgents = previousAgentsFromStates(cwd);
103
+
104
+ const agents = resolveAgents(opts.agent, detectedAgents, previousAgents);
105
+ const previousStates = {};
106
+ for (const agent of agents) {
107
+ previousStates[agent] = loadState(cwd, agent);
108
+ }
109
+
110
+ const previousOpted = previouslyOptedConditional(previousStates, agents);
111
+ const autoOpted = pickConditionalAuto(features, agents);
112
+ const defaultOpted = previousOpted.length ? previousOpted : autoOpted;
113
+
114
+ let optedSkills;
115
+ if (interactive) {
116
+ p.intro(`agentic update${dryRun ? ' (dry-run)' : ''}${force ? ' (force)' : ''}`);
117
+ const previousLine = previousAgents.length
118
+ ? previousAgents.map((a) => AGENT_LABEL[a]).join(', ')
119
+ : 'none — first update on a legacy install';
120
+ p.note(
121
+ `Previous install: ${previousLine}\n` +
122
+ `Updating for: ${agents.map((a) => AGENT_LABEL[a]).join(' + ')}\n` +
123
+ `Kit version: ${pkg.version}`,
124
+ 'Update plan'
125
+ );
126
+
127
+ const conditionalOptions = CONDITIONAL_SKILLS.filter((s) =>
128
+ s.agents.some((a) => agents.includes(a))
129
+ ).map((s) => ({
130
+ value: s.name,
131
+ label: s.name,
132
+ hint: s.autoIf(features) ? s.hintWhenAuto : s.hintWhenManual,
133
+ }));
134
+
135
+ if (conditionalOptions.length > 0) {
136
+ const picked = await p.multiselect({
137
+ message: 'Optional skills (toggle to include or exclude):',
138
+ options: conditionalOptions,
139
+ initialValues: defaultOpted,
140
+ required: false,
141
+ });
142
+ if (p.isCancel(picked)) {
143
+ p.cancel('Cancelled.');
144
+ return;
145
+ }
146
+ optedSkills = picked;
147
+ } else {
148
+ optedSkills = [];
149
+ }
150
+ } else {
151
+ optedSkills = defaultOpted;
152
+ }
153
+
154
+ const confirmReplace = interactive
155
+ ? async (question) => {
156
+ const answer = await p.confirm({ message: question, initialValue: false });
157
+ if (p.isCancel(answer)) return false;
158
+ return answer;
159
+ }
160
+ : async () => Boolean(force);
161
+
162
+ const confirmRemove = interactive
163
+ ? async (question) => {
164
+ const answer = await p.confirm({ message: question, initialValue: false });
165
+ if (p.isCancel(answer)) return false;
166
+ return answer;
167
+ }
168
+ : async () => Boolean(force);
169
+
170
+ const installedSkillSet = new Set();
171
+ const allActions = [];
172
+ const nextStates = {};
173
+
174
+ for (const agent of agents) {
175
+ const agentSkills = skillsForAgent(agent, optedSkills);
176
+ for (const s of agentSkills) installedSkillSet.add(s);
177
+
178
+ const orphanResult = await removeOrphanSkills({
179
+ cwd,
180
+ agent,
181
+ previousState: previousStates[agent],
182
+ currentSkills: agentSkills,
183
+ confirmRemove,
184
+ dryRun,
185
+ });
186
+ allActions.push(...orphanResult.actions);
187
+
188
+ const result = await installSkills({
189
+ cwd,
190
+ agents: [agent],
191
+ skills: agentSkills,
192
+ confirmReplace,
193
+ previousStates: { [agent]: previousStates[agent] ?? null },
194
+ kitVersion: pkg.version,
195
+ dryRun,
196
+ force,
197
+ });
198
+ allActions.push(...result.actions);
199
+ nextStates[agent] = result.nextStates[agent];
200
+ }
201
+
202
+ if (!dryRun) {
203
+ for (const agent of agents) {
204
+ saveState(cwd, agent, nextStates[agent]);
205
+ }
206
+ }
207
+
208
+ const skillDisplayOrder = [
209
+ ...REQUIRED_SKILLS,
210
+ ...CONDITIONAL_SKILLS.map((s) => s.name),
211
+ ].filter((s) => installedSkillSet.has(s));
212
+
213
+ const confirmAppend = interactive
214
+ ? async (path) => {
215
+ const answer = await p.confirm({
216
+ message: `Append a managed "Skills installed by agentic" section to ${path}? (existing content preserved)`,
217
+ initialValue: true,
218
+ });
219
+ if (p.isCancel(answer)) return false;
220
+ return answer;
221
+ }
222
+ : async () => true;
223
+
224
+ const rootDocAction = !dryRun
225
+ ? await updateRootDoc({
226
+ cwd,
227
+ skills: skillDisplayOrder,
228
+ confirmAppend,
229
+ })
230
+ : { type: 'absent', path: null };
231
+
232
+ const lines = allActions.map((a) => {
233
+ const sym = ACTION_SYMBOL[a.type] ?? '?';
234
+ return `${sym} [${a.agent}] ${a.path}`;
235
+ });
236
+ if (rootDocAction.type !== 'absent') {
237
+ lines.push(`${ROOT_DOC_LABEL[rootDocAction.type]}${rootDocAction.path}`);
238
+ }
239
+
240
+ if (interactive) {
241
+ p.note(lines.join('\n') || '(no changes)', dryRun ? 'Plan' : 'Result');
242
+ const closing = dryRun
243
+ ? 'Dry-run only — nothing written. Re-run without --dry-run to apply.'
244
+ : `Updated to ${pkg.version}. State saved at .claude/agentic-state.json / .agents/agentic-state.json.`;
245
+ p.outro(closing);
246
+ } else {
247
+ for (const line of lines) {
248
+ process.stderr.write(`${line}\n`);
249
+ }
250
+ }
251
+ }
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { initCommand } from './commands/init.js';
6
+ import { updateCommand } from './commands/update.js';
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
  const pkg = JSON.parse(
@@ -24,5 +25,14 @@ export async function run(argv) {
24
25
  .option('-y, --yes', 'skip confirmation prompts (non-interactive)')
25
26
  .action(initCommand);
26
27
 
28
+ program
29
+ .command('update')
30
+ .description('Pull upstream kit changes into an installed project (three-way diff against the saved state)')
31
+ .option('-a, --agent <agent>', 'restrict update to a specific agent: claude-code | codex | both')
32
+ .option('-y, --yes', 'skip confirmation prompts (non-interactive)')
33
+ .option('--dry-run', 'preview the action plan without writing any files')
34
+ .option('--force', 'overwrite user-edited files on conflict (non-interactive default: no)')
35
+ .action(updateCommand);
36
+
27
37
  await program.parseAsync(argv);
28
38
  }
@@ -1,13 +1,17 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import {
2
3
  copyFileSync,
3
4
  existsSync,
4
5
  mkdirSync,
5
6
  readFileSync,
6
7
  readdirSync,
8
+ rmSync,
7
9
  statSync,
10
+ unlinkSync,
8
11
  } from 'node:fs';
9
12
  import { fileURLToPath } from 'node:url';
10
13
  import { basename, dirname, join, relative } from 'node:path';
14
+ import { SCHEMA_VERSION } from './state.js';
11
15
 
12
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
17
  const KIT_ROOT = join(__dirname, '..', '..');
@@ -26,6 +30,12 @@ const AGENT_LAYOUT = {
26
30
  },
27
31
  };
28
32
 
33
+ export function agentLayout(agent) {
34
+ const layout = AGENT_LAYOUT[agent];
35
+ if (!layout) throw new Error(`unknown agent "${agent}"`);
36
+ return layout;
37
+ }
38
+
29
39
  function walkSkill(srcRoot) {
30
40
  const out = [];
31
41
  function walk(dir, prefix) {
@@ -55,94 +65,296 @@ function loadManifest(srcRoot) {
55
65
  return { subagents: Array.isArray(raw.subagents) ? raw.subagents : [] };
56
66
  }
57
67
 
68
+ function sha256Of(path) {
69
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
70
+ }
71
+
58
72
  function sameFile(a, b) {
59
73
  if (!existsSync(b)) return false;
60
74
  return readFileSync(a).equals(readFileSync(b));
61
75
  }
62
76
 
77
+ function targetForRel(rel, layout, targetRoot, cwd, subagentSet) {
78
+ if (subagentSet.has(rel)) {
79
+ if (!layout.agentsDir) return null;
80
+ return join(cwd, layout.agentsDir, basename(rel));
81
+ }
82
+ return join(targetRoot, rel);
83
+ }
84
+
85
+ function resolveSkillSource(agent, skill) {
86
+ const layout = agentLayout(agent);
87
+ const srcRoot = join(KIT_ROOT, layout.sourceDir, skill);
88
+ if (!existsSync(srcRoot)) {
89
+ throw new Error(
90
+ `skill "${skill}" not found for agent "${agent}" (expected at ${srcRoot})`
91
+ );
92
+ }
93
+ const manifest = loadManifest(srcRoot);
94
+ const subagentSet = new Set(manifest.subagents);
95
+ const walked = walkSkill(srcRoot);
96
+ const walkedRels = new Set(walked.map(({ rel }) => rel));
97
+ for (const declared of subagentSet) {
98
+ if (!walkedRels.has(declared)) {
99
+ throw new Error(
100
+ `manifest at ${join(srcRoot, MANIFEST_FILE)} declares subagent "${declared}" but no such file exists in the skill source`
101
+ );
102
+ }
103
+ }
104
+ return { layout, srcRoot, subagentSet, walked };
105
+ }
106
+
107
+ function planFile({
108
+ src,
109
+ rel,
110
+ target,
111
+ relForReport,
112
+ prevSha,
113
+ force,
114
+ }) {
115
+ const sourceSha = sha256Of(src);
116
+ if (!existsSync(target)) {
117
+ return { type: 'create', sourceSha };
118
+ }
119
+ const targetSha = sha256Of(target);
120
+
121
+ if (sourceSha === targetSha) {
122
+ return { type: 'unchanged', sourceSha };
123
+ }
124
+
125
+ if (prevSha === undefined || prevSha === null) {
126
+ return { type: 'legacy-divergent', sourceSha, targetSha };
127
+ }
128
+
129
+ if (sourceSha === prevSha) {
130
+ // kit unchanged since last install; differ vs. target → user edited; keep
131
+ return { type: 'user-edited-keep', sourceSha };
132
+ }
133
+
134
+ if (targetSha === prevSha) {
135
+ // user untouched; kit changed → silent update
136
+ return { type: 'kit-changed-update', sourceSha };
137
+ }
138
+
139
+ // both changed → conflict
140
+ if (force) return { type: 'conflict-force', sourceSha };
141
+ return { type: 'conflict-prompt', sourceSha, targetSha };
142
+ }
143
+
63
144
  /**
64
- * Install one or more skills for one or more agents.
145
+ * Install one or more skills for one or more agents, with optional state-aware
146
+ * three-way diff against the prior install.
147
+ *
148
+ * Legacy contract (no state): default-skip on divergent files, prompt via
149
+ * confirmReplace. Matches v0.2 behavior.
150
+ *
151
+ * State-aware contract (previousStates supplied): three-way diff using the
152
+ * previous source SHA, target SHA, current source SHA. See ADR-0009.
65
153
  *
66
154
  * @param {object} opts
67
- * @param {string} opts.cwd Target project root.
68
- * @param {string[]} opts.agents e.g. ['claude-code', 'codex'].
69
- * @param {string[]} opts.skills Skill names, e.g. ['agentic-bootstrap'].
155
+ * @param {string} opts.cwd
156
+ * @param {string[]} opts.agents
157
+ * @param {string[]} opts.skills
70
158
  * @param {(question: string) => Promise<boolean>} [opts.confirmReplace]
71
- * Async callback used when a target file already exists with different
72
- * content. Default: skip without asking (safe non-interactive default).
159
+ * Callback for divergent files (legacy path or three-way conflict). Default: skip.
160
+ * @param {Record<string, object|null>} [opts.previousStates]
161
+ * Per-agent previous state from src/lib/state.js. Missing/null entries fall
162
+ * back to legacy byte-compare.
163
+ * @param {string} [opts.kitVersion]
164
+ * Stamped into the returned nextStates as kitVersion and per-skill version.
165
+ * @param {boolean} [opts.dryRun]
166
+ * If true, no files are written. Actions reflect what would happen.
167
+ * @param {boolean} [opts.force]
168
+ * If true, three-way conflicts overwrite without prompting.
73
169
  *
74
- * @returns {Promise<{ actions: Array<{type: 'created'|'replaced'|'unchanged'|'skipped', path: string}> }>}
170
+ * @returns {Promise<{ actions: Array<{type, path, agent}>, nextStates: Record<string, object> }>}
75
171
  */
76
172
  export async function installSkills({
77
173
  cwd,
78
174
  agents,
79
175
  skills,
80
176
  confirmReplace = async () => false,
177
+ previousStates = {},
178
+ kitVersion = null,
179
+ dryRun = false,
180
+ force = false,
81
181
  }) {
82
182
  const actions = [];
183
+ const nextStates = {};
83
184
 
84
185
  for (const agent of agents) {
85
- const layout = AGENT_LAYOUT[agent];
86
- if (!layout) {
87
- throw new Error(`unknown agent "${agent}"`);
88
- }
186
+ const prev = previousStates[agent] ?? null;
187
+ const nextSkills = {};
89
188
 
90
189
  for (const skill of skills) {
91
- const srcRoot = join(KIT_ROOT, layout.sourceDir, skill);
92
- if (!existsSync(srcRoot)) {
93
- throw new Error(
94
- `skill "${skill}" not found for agent "${agent}" (expected at ${srcRoot})`
95
- );
96
- }
97
-
190
+ const { layout, srcRoot, subagentSet, walked } = resolveSkillSource(agent, skill);
98
191
  const targetRoot = join(cwd, layout.skillsDir, skill);
99
- const manifest = loadManifest(srcRoot);
100
- const subagentSet = new Set(manifest.subagents);
101
- const walked = walkSkill(srcRoot);
102
- const walkedRels = new Set(walked.map(({ rel }) => rel));
103
- for (const declared of subagentSet) {
104
- if (!walkedRels.has(declared)) {
105
- throw new Error(
106
- `manifest at ${join(srcRoot, MANIFEST_FILE)} declares subagent "${declared}" but no such file exists in the skill source`
107
- );
108
- }
109
- }
192
+ const prevSkill = prev?.skills?.[skill] ?? null;
193
+ const prevByPath = new Map(
194
+ (prevSkill?.files ?? []).map((f) => [f.path, f.sourceSha])
195
+ );
196
+ const skillFiles = [];
110
197
 
111
198
  for (const { src, rel } of walked) {
112
199
  if (rel === MANIFEST_FILE) continue;
113
200
 
114
- let target;
115
- if (subagentSet.has(rel)) {
116
- if (!layout.agentsDir) continue;
117
- target = join(cwd, layout.agentsDir, basename(rel));
118
- } else {
119
- target = join(targetRoot, rel);
120
- }
201
+ const target = targetForRel(rel, layout, targetRoot, cwd, subagentSet);
202
+ if (!target) continue;
121
203
  const relForReport = relative(cwd, target);
204
+ const prevSha = prevByPath.has(relForReport)
205
+ ? prevByPath.get(relForReport)
206
+ : null;
207
+
208
+ const decision = planFile({
209
+ src,
210
+ rel,
211
+ target,
212
+ relForReport,
213
+ prevSha,
214
+ force,
215
+ });
122
216
 
123
- if (existsSync(target)) {
124
- if (sameFile(src, target)) {
125
- actions.push({ type: 'unchanged', path: relForReport });
126
- continue;
217
+ let actionType;
218
+ let writeFile = false;
219
+
220
+ switch (decision.type) {
221
+ case 'create':
222
+ actionType = 'created';
223
+ writeFile = true;
224
+ break;
225
+ case 'unchanged':
226
+ actionType = 'unchanged';
227
+ break;
228
+ case 'kit-changed-update':
229
+ actionType = 'updated';
230
+ writeFile = true;
231
+ break;
232
+ case 'user-edited-keep':
233
+ actionType = 'kept';
234
+ break;
235
+ case 'conflict-force':
236
+ actionType = 'replaced';
237
+ writeFile = true;
238
+ break;
239
+ case 'conflict-prompt': {
240
+ const replace = await confirmReplace(
241
+ `${relForReport}: kit and your edits both changed since last install. Overwrite?`
242
+ );
243
+ if (replace) {
244
+ actionType = 'replaced';
245
+ writeFile = true;
246
+ } else {
247
+ actionType = 'skipped';
248
+ }
249
+ break;
127
250
  }
128
- const replace = await confirmReplace(
129
- `Replace ${relForReport}? (target differs from the kit version)`
130
- );
131
- if (!replace) {
132
- actions.push({ type: 'skipped', path: relForReport });
133
- continue;
251
+ case 'legacy-divergent': {
252
+ const replace = await confirmReplace(
253
+ `Replace ${relForReport}? (target differs from the kit version)`
254
+ );
255
+ if (replace) {
256
+ actionType = 'replaced';
257
+ writeFile = true;
258
+ } else {
259
+ actionType = 'skipped';
260
+ }
261
+ break;
134
262
  }
263
+ default:
264
+ throw new Error(`unhandled plan decision: ${decision.type}`);
265
+ }
266
+
267
+ if (writeFile && !dryRun) {
135
268
  mkdirSync(dirname(target), { recursive: true });
136
269
  copyFileSync(src, target);
137
- actions.push({ type: 'replaced', path: relForReport });
138
- } else {
139
- mkdirSync(dirname(target), { recursive: true });
140
- copyFileSync(src, target);
141
- actions.push({ type: 'created', path: relForReport });
142
270
  }
271
+
272
+ // Record the current source SHA on every outcome — including skip.
273
+ // A skip means "I have seen this kit version and chose to keep my
274
+ // edits"; the next run should treat the same kit version as already
275
+ // acknowledged (silent user-edited-keep). If we recorded the prior
276
+ // SHA on skip, every re-run with the kit unchanged would re-prompt
277
+ // the same conflict the user already declined.
278
+ actions.push({ type: actionType, path: relForReport, agent });
279
+ skillFiles.push({ path: relForReport, sourceSha: decision.sourceSha });
280
+ }
281
+
282
+ nextSkills[skill] = {
283
+ version: kitVersion ?? prevSkill?.version ?? null,
284
+ files: skillFiles,
285
+ };
286
+ }
287
+
288
+ nextStates[agent] = {
289
+ schemaVersion: SCHEMA_VERSION,
290
+ kitVersion: kitVersion ?? prev?.kitVersion ?? null,
291
+ agent,
292
+ skills: nextSkills,
293
+ };
294
+ }
295
+
296
+ return { actions, nextStates };
297
+ }
298
+
299
+ /**
300
+ * Identify and optionally remove orphan skills — skills present in the prior
301
+ * state but absent from the current opted skill list. Default keep.
302
+ *
303
+ * @param {object} opts
304
+ * @param {string} opts.cwd
305
+ * @param {string} opts.agent
306
+ * @param {object|null} opts.previousState
307
+ * @param {string[]} opts.currentSkills
308
+ * @param {(question: string) => Promise<boolean>} [opts.confirmRemove]
309
+ * @param {boolean} [opts.dryRun]
310
+ *
311
+ * @returns {Promise<{ actions: Array<{type, path, agent}>, removedSkills: string[] }>}
312
+ */
313
+ export async function removeOrphanSkills({
314
+ cwd,
315
+ agent,
316
+ previousState,
317
+ currentSkills,
318
+ confirmRemove = async () => false,
319
+ dryRun = false,
320
+ }) {
321
+ const actions = [];
322
+ const removedSkills = [];
323
+ if (!previousState?.skills) return { actions, removedSkills };
324
+
325
+ const layout = agentLayout(agent);
326
+ const currentSet = new Set(currentSkills);
327
+
328
+ for (const [skill, entry] of Object.entries(previousState.skills)) {
329
+ if (currentSet.has(skill)) continue;
330
+ const remove = await confirmRemove(
331
+ `Remove orphan skill "${skill}" for ${agent}? (no longer in your install set)`
332
+ );
333
+ if (!remove) {
334
+ for (const f of entry.files) {
335
+ actions.push({ type: 'orphan-kept', path: f.path, agent });
336
+ }
337
+ continue;
338
+ }
339
+
340
+ for (const f of entry.files) {
341
+ const abs = join(cwd, f.path);
342
+ if (existsSync(abs) && !dryRun) {
343
+ unlinkSync(abs);
344
+ }
345
+ actions.push({ type: 'removed', path: f.path, agent });
346
+ }
347
+ // Try to drop the empty skill directory under skillsDir/<skill>.
348
+ const skillDir = join(cwd, layout.skillsDir, skill);
349
+ if (!dryRun && existsSync(skillDir)) {
350
+ try {
351
+ rmSync(skillDir, { recursive: true, force: true });
352
+ } catch {
353
+ // best-effort cleanup
143
354
  }
144
355
  }
356
+ removedSkills.push(skill);
145
357
  }
146
358
 
147
- return { actions };
359
+ return { actions, removedSkills };
148
360
  }
@@ -0,0 +1,80 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+
4
+ export const SCHEMA_VERSION = 1;
5
+ export const STATE_FILE = 'agentic-state.json';
6
+
7
+ export const STATE_DIRS = {
8
+ 'claude-code': '.claude',
9
+ codex: '.agents',
10
+ };
11
+
12
+ export function statePath(cwd, agent) {
13
+ const dir = STATE_DIRS[agent];
14
+ if (!dir) throw new Error(`unknown agent "${agent}"`);
15
+ return join(cwd, dir, STATE_FILE);
16
+ }
17
+
18
+ export function emptyState(agent, kitVersion) {
19
+ return {
20
+ schemaVersion: SCHEMA_VERSION,
21
+ kitVersion,
22
+ agent,
23
+ skills: {},
24
+ };
25
+ }
26
+
27
+ export function loadState(cwd, agent) {
28
+ const path = statePath(cwd, agent);
29
+ if (!existsSync(path)) return null;
30
+ let raw;
31
+ try {
32
+ raw = JSON.parse(readFileSync(path, 'utf8'));
33
+ } catch (err) {
34
+ throw new Error(`malformed state at ${path}: ${err.message}`);
35
+ }
36
+ if (typeof raw.schemaVersion !== 'number') {
37
+ throw new Error(`state at ${path} missing schemaVersion`);
38
+ }
39
+ if (raw.schemaVersion > SCHEMA_VERSION) {
40
+ throw new Error(
41
+ `state at ${path} has schemaVersion ${raw.schemaVersion}; this kit only knows ${SCHEMA_VERSION}. Upgrade the kit.`
42
+ );
43
+ }
44
+ if (raw.agent && raw.agent !== agent) {
45
+ throw new Error(
46
+ `state at ${path} declares agent "${raw.agent}" but expected "${agent}"`
47
+ );
48
+ }
49
+ return {
50
+ schemaVersion: raw.schemaVersion,
51
+ kitVersion: raw.kitVersion ?? null,
52
+ agent,
53
+ skills: raw.skills ?? {},
54
+ };
55
+ }
56
+
57
+ export function saveState(cwd, agent, state) {
58
+ const path = statePath(cwd, agent);
59
+ mkdirSync(dirname(path), { recursive: true });
60
+ const ordered = orderState(state);
61
+ writeFileSync(path, JSON.stringify(ordered, null, 2) + '\n');
62
+ }
63
+
64
+ function orderState(state) {
65
+ const skills = {};
66
+ for (const skillName of Object.keys(state.skills).sort()) {
67
+ const entry = state.skills[skillName];
68
+ const files = [...entry.files].sort((a, b) => a.path.localeCompare(b.path));
69
+ skills[skillName] = {
70
+ version: entry.version,
71
+ files: files.map((f) => ({ path: f.path, sourceSha: f.sourceSha })),
72
+ };
73
+ }
74
+ return {
75
+ schemaVersion: state.schemaVersion,
76
+ kitVersion: state.kitVersion,
77
+ agent: state.agent,
78
+ skills,
79
+ };
80
+ }
@@ -60,3 +60,10 @@ Stop after writing. Do **not** flip status to `accepted` — that requires user
60
60
  ## Output contract
61
61
 
62
62
  A single new file at `doc/adr/<NNNN>-<short-slug>.md`. Status `proposed`. No existing ADRs modified. No invented content.
63
+
64
+ ADRs are decision-record artifacts and are **exempt** from the no-dates rule (Documentation Discipline §2): the `**Status:**` lifecycle and `**Date:**` field are required for Nygard supersession ordering. The remaining Documentation Discipline rules (`WORKFLOW.md` §2) apply at write time:
65
+
66
+ - No emoji anywhere in the file.
67
+ - `Context` is the business-context-first section — the *forces* and *problem* before the *decision*.
68
+ - One scope: one decision per ADR. If the user's request implies multiple decisions, ask which one to write first; the others become follow-up ADRs.
69
+ - No speculation. `Decision` is a directive ("We will…"); rejected paths go in `Alternatives Considered`, not the body.
@@ -102,3 +102,11 @@ Currently-binding decisions. Link each to `doc/adr/`.
102
102
  ## Output contract
103
103
 
104
104
  A single `ARCHITECTURE.md` at the repo root. Every line locks a binding pattern. ADR candidates flagged in the response, not written. In audit mode: drift list only, no file written.
105
+
106
+ `ARCHITECTURE.md` is a narrative document, so the Documentation Discipline rules in `WORKFLOW.md` §2 apply at write time:
107
+
108
+ - No emoji anywhere in the file.
109
+ - No dates, version stamps, `DRAFT` markers, or changelog blocks. Architecture is the current binding pattern; the lifecycle of "when did this become true" lives in ADRs and git history.
110
+ - `Overview` is the business-context-first paragraph — *what the system does* and *what would break without it* before layers and patterns.
111
+ - One scope: system-level patterns and boundaries. Per-decision rationale lives in ADRs; per-project operations live in `AGENTS.md`. Link, do not copy.
112
+ - No speculation. If a layer has no observed signal, write `<TODO: not yet wired>` once; do not propose patterns that aren't already in the code.
@@ -34,6 +34,21 @@ If the user names an artifact (`AGENTS.md`, `ARCHITECTURE.md`, ADRs), audit only
34
34
  * Status field — every ADR has one of `proposed | accepted | deprecated | superseded by ADR-NNNN`.
35
35
  * Superseded chains — every "superseded by ADR-NNNN" target exists.
36
36
 
37
+ ### Documentation discipline drift (`WORKFLOW.md` §2 / ADR-0008)
38
+
39
+ Audit narrative documents — `README.md`, `AGENTS.md` / `CLAUDE.md`, `ARCHITECTURE.md`, `DESIGN.md`, any prose page under `doc/` that is not a decision-record artifact under `doc/adr/` or `doc/tasks/`:
40
+
41
+ * Emoji — any present? Rule 3 forbids emoji anywhere (docs, code, comments, commits, skill outputs).
42
+ * Dates / version stamps / `DRAFT` markers / changelog blocks in narrative documents — Rule 2 forbids these. Decision-record artifacts under `doc/adr/` and `doc/tasks/` are exempt.
43
+ * Business context first — does the first paragraph answer *why* the document exists, before *what* and *how*? Rule 4.
44
+ * Scope duplication — does the document copy material that is canonically owned by another file (`AGENTS.md` repeating `ARCHITECTURE.md` patterns; `README.md` re-stating ADR rationale)? Rule 5 requires linking, not copying.
45
+ * Speculation — phrases like "we might", "in the future", "could be added", or roadmaps without an ADR / task reference. Rule 1 forbids unfounded plans.
46
+
47
+ Source code (sample, not exhaustive — flag findings, not every match):
48
+
49
+ * Orphan `TODO` / `FIXME` — Rule 7. A reference to a GitHub Issue or a `doc/tasks/NNNN-*.md` task file makes it not orphan.
50
+ * Commented-out code blocks — Rule 7. Removed code lives in git history.
51
+
37
52
  ## Step 3 — Output
38
53
 
39
54
  One line per finding, formatted:
@@ -42,7 +57,7 @@ One line per finding, formatted:
42
57
  [file or section]: spec says X, code says Y. Suggested resolution: change spec / change code / discuss.
43
58
  ```
44
59
 
45
- Group by artifact. If a category has no drift, print one line: `AGENTS.md — no drift.` etc. If an audited artifact does not exist, say so explicitly rather than reporting zero findings.
60
+ Group by artifact. If a category has no drift, print one line: `AGENTS.md — no drift.` etc. If an audited artifact does not exist, say so explicitly rather than reporting zero findings. The Documentation discipline drift category groups its own findings under `Documentation discipline — <category>: ...`.
46
61
 
47
62
  If something the user says contradicts what the code shows, surface the conflict. Don't silently trust the user; don't silently trust the code.
48
63
 
@@ -151,3 +151,11 @@ Real traps. Each one should map to an incident or to specific code.
151
151
  ## Output contract
152
152
 
153
153
  A single `AGENTS.md` at the repo root, ≤150 lines, every line operational. No "External Resources" section. No appended Universal Agent Behavior block — that lives in the `agentic-philosophy` skill. No meta-prose explaining gaps. In audit mode: a drift list, no file written.
154
+
155
+ `AGENTS.md` is a narrative document, so the Documentation Discipline rules in `WORKFLOW.md` §2 apply at write time:
156
+
157
+ - No emoji anywhere in the file.
158
+ - No dates, version stamps, `DRAFT` markers, or changelog blocks. Stack versions are facts (`Node ≥18`); release timelines are not.
159
+ - `Project Overview` is the business-context-first paragraph — *why* the project exists before *what* it does.
160
+ - One scope: this file is the operational guide for agents. Do not duplicate `ARCHITECTURE.md` patterns or ADR rationale here; link instead.
161
+ - No speculation. If a section has no signal, write `<TODO: not yet wired>` once; do not narrate "this could be added later".
@@ -61,3 +61,11 @@ If the source has tokens DESIGN.md doesn't document, list those too as additions
61
61
  ## Output contract
62
62
 
63
63
  A single `DESIGN.md` at the repo root. YAML frontmatter uses W3C `$value`/`$type` shape. Markdown body has one section per token group present in the source. No invented tokens. No "External Resources" section. In audit mode: a drift list, no file written.
64
+
65
+ `DESIGN.md` is a narrative document, so the Documentation Discipline rules in `WORKFLOW.md` §2 apply at write time:
66
+
67
+ - No emoji anywhere — including the Markdown body's do's and don'ts.
68
+ - No dates, version stamps, `DRAFT` markers, or changelog blocks. Token revisions live in git history; DESIGN.md is the current visual contract.
69
+ - The Markdown body opens with the *why* of each token group — the visual constraint or product principle — before listing rules.
70
+ - One scope: visual contract. Component anatomy and interaction patterns belong elsewhere; link, do not copy.
71
+ - No speculation. If a group has no source token, mark `<TODO: not yet wired>` and move on.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: agentic-philosophy
3
- description: Universal agent behavior — think before coding, ground in real patterns, prefer simplicity, make surgical changes, define verifiable goals, verify before claiming done. Auto-invokes on non-trivial changes, refactors, debugging, "think before coding", "ground before coding", "verify done", "before implementing", or whenever the task is ambiguous enough that guardrails matter.
3
+ description: Universal agent behavior and documentation discipline — think before coding, ground in real patterns, prefer simplicity, make surgical changes, define verifiable goals, verify before claiming done, and write documentation that captures only definitions and decisions. Auto-invokes on non-trivial changes, refactors, debugging, "think before coding", "ground before coding", "verify done", "before implementing", on documentation work — "writing docs", "writing readme", "writing architecture", "writing adr", "writing task", "audit docs" — or whenever the task is ambiguous enough that guardrails matter.
4
4
  ---
5
5
 
6
6
  # /agentic-philosophy
7
7
 
8
- Six behaviors apply to every non-trivial change. Bias toward caution over speed; for trivial diffs, use judgment.
8
+ Six behaviors apply to every non-trivial change. Bias toward caution over speed; for trivial diffs, use judgment. A separate Documentation Discipline section at the end applies to every document the agent writes.
9
9
 
10
10
  ## Think Before Coding
11
11
 
@@ -86,3 +86,18 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
86
86
  - For UI/runtime changes, exercise the feature in a browser.
87
87
  - Can't verify it? Say so. Don't claim success.
88
88
  - Never bypass gates (`--no-verify`, skipped hooks, deleted failing tests).
89
+
90
+ ## Documentation Discipline
91
+
92
+ **Every document the agent writes obeys these eight rules.** The canonical source is `WORKFLOW.md` §2. ADR-0008 records the decision and its reconciliations.
93
+
94
+ 1. **Definitions and decisions only.** Capture what is true now and the decisions that brought it there. No speculation, no history, no unfounded plans. A deferred decision is in scope when it is *recorded* — an accepted ADR or a task file is fundamentação; "we might do X later" without a record is speculation and is cut.
95
+ 2. **No dates, version stamps, `DRAFT` markers, or changelogs in narrative documents.** Applies to `README.md`, `AGENTS.md` / `CLAUDE.md`, `ARCHITECTURE.md`, `DESIGN.md`, specs, and any prose page. **Decision-record artifacts are exempt** — ADRs under `doc/adr/` keep their `**Status:**` and `**Date:**` fields (Nygard supersession requires ordering); tasks under `doc/tasks/` keep their `**Created:**` field and append-only dated `Notes` log. Outside those artifacts, use git history.
96
+ 3. **No emoji anywhere.** Not in docs, code, source comments, commit messages, PR bodies, or skill outputs. Severity and status use words; structural cues use Markdown.
97
+ 4. **Business context first.** Open every document with *why* — the problem, the constraint, the user — before *what* and *how*. The first paragraph must answer "what would break if this document didn't exist".
98
+ 5. **One scope per document. No duplication.** If two documents would say the same thing, link instead of copying. The canonical location owns the content; everywhere else references it.
99
+ 6. **Code is the primary documentation of behavior.** Comments justify *why* a non-obvious choice was made — never restate *what*. If the comment is needed to explain *what*, rename or refactor.
100
+ 7. **No commented-out code; no orphan `TODO` / `FIXME` in source.** Every deferred item references a tracked work item — a GitHub Issue, or a per-task file under `doc/tasks/NNNN-*.md`. The trace must be addressable from the source line.
101
+ 8. **Tests are living documentation of behavior.** Test names and assertions read as the spec they enforce. Spec changes drive test changes; never the reverse.
102
+
103
+ When generating or auditing a document, walk this list before declaring done.
@@ -31,16 +31,22 @@ The reviewer subagent will get **only** what you assemble here. No conversation
31
31
 
32
32
  Build the handoff as a single message: a short framing paragraph (what's being reviewed, what spec applies), followed by the diff and the spec slice. No prose summary of what you think the diff does — the reviewer reads the diff itself.
33
33
 
34
- ## Step 2 — Delegate to the subagent
34
+ ## Step 2 — Persist the handoff to disk
35
35
 
36
- Invoke the bundled `fresh-context-reviewer` subagent via the `Task` tool. Pass the assembled handoff as the prompt. The subagent has read-only tools (`Read, Glob, Grep, Bash`) and no write access — it cannot accidentally modify the code under review.
36
+ Write the assembled handoff to `.agentic/reviews/<ISO-timestamp>-<scope-slug>.md` at the repo root. The path encodes both the moment of review and a short slug for the scope (`branch-vs-main`, `pr-42`, `commit-abc1234`, `working-tree`). Create the directory if it does not exist. The file is the audit trail the user can read it later, replay the review against an updated diff, or share it with a teammate.
37
37
 
38
- ## Step 3 Surface findings
38
+ This directory is ephemeral; advise the user to add `.agentic/reviews/` to their `.gitignore` if it is not already. Handoffs are per-review artifacts, not committed history.
39
39
 
40
- Relay the subagent's findings to the user verbatim, grouped by severity (Blocker / Concern / Note). Do **not** add commentary defending the code. Do **not** synthesize an "approve" verdict §10 frames the reviewer as adversarial; approval is the user's call after weighing the findings.
40
+ ## Step 3Delegate to the subagent
41
+
42
+ Invoke the bundled `fresh-context-reviewer` subagent via the `Task` tool. Pass the assembled handoff as the prompt — the same content you wrote to disk. The subagent has read-only tools (`Read, Glob, Grep, Bash`) and no write access; it cannot accidentally modify the code under review.
43
+
44
+ ## Step 4 — Surface findings
45
+
46
+ Relay the subagent's findings to the user verbatim, grouped by severity (Blocker / Concern / Note). Do **not** add commentary defending the code. Do **not** synthesize an "approve" verdict — §10 frames the reviewer as adversarial; approval is the user's call after weighing the findings. Reference the persisted handoff path in your reply so the user can audit what was sent.
41
47
 
42
48
  If the subagent reports zero findings across all severities, say so explicitly ("no issues found in <range>"). Empty results are real signal, not a gap.
43
49
 
44
50
  ## Output contract
45
51
 
46
- A structured findings list grouped Blocker / Concern / Note, each finding `file:line: <emoji> <severity>: <problem>. <fix>.`. No "approve" verdict, no defending of the code, no rewrite of the diff. Empty result is reported explicitly.
52
+ A structured findings list grouped Blocker / Concern / Note, each finding `file:line: <severity>: <problem>. <fix>.`. The path of the persisted handoff under `.agentic/reviews/` is reported alongside. No "approve" verdict, no defending of the code, no rewrite of the diff. Empty result is reported explicitly.
@@ -37,9 +37,7 @@ Group findings by severity:
37
37
  - **Concern** — worth a follow-up task. Real issue, not blocking the current change.
38
38
  - **Note** — informational, no action expected. Includes "no issues found in this section".
39
39
 
40
- Each finding: one line, `file:line: <emoji> <severity>: <problem>. <fix>.`
41
-
42
- Use `🚨` for Blocker, `⚠️` for Concern, `ℹ️` for Note.
40
+ Each finding: one line, `file:line: <severity>: <problem>. <fix>.` Severity is the literal word `Blocker`, `Concern`, or `Note`.
43
41
 
44
42
  End with a one-line bottom-line: `Ship as-is`, `Ship with the Concerns logged as follow-up tasks`, or `Don't ship until Blockers resolved`.
45
43
 
@@ -91,3 +91,11 @@ All Acceptance Criteria checked, plus:
91
91
  ## Output contract
92
92
 
93
93
  A single new file at `doc/tasks/<NNNN>-<short-slug>.md`. Status `proposed`. Notes empty. No existing tasks modified. No invented values.
94
+
95
+ Task files are decision-record artifacts and are **exempt** from the no-dates rule (Documentation Discipline §2): the `**Created:**` field anchors the task in time and the append-only `Notes` log is dated per entry by design. The remaining Documentation Discipline rules (`WORKFLOW.md` §2) apply at write time:
96
+
97
+ - No emoji anywhere in the file.
98
+ - `Context` is the business-context-first section — *why this task exists* and *what would break without it* before *Acceptance Criteria*.
99
+ - One scope: one task per file. If the user's request implies multiple deliverables, ask which to write first; the others become follow-up tasks.
100
+ - No speculation. Acceptance criteria must be measurable; do not list aspirational items ("loads in under 2s", not "fast enough").
101
+ - `Notes` is append-only and dated per entry — that is the auditability primitive, not a violation of Rule 2.
@@ -50,4 +50,10 @@ Stop after writing. Do NOT flip status to accepted — that requires user review
50
50
 
51
51
  <output_contract>
52
52
  A single new file at `doc/adr/<NNNN>-<short-slug>.md`. Status proposed. No existing ADRs modified. No invented content.
53
+
54
+ ADRs are decision-record artifacts and are exempt from the no-dates rule (Documentation Discipline §2): `**Status:**` and `**Date:**` are required for Nygard supersession ordering. Remaining Documentation Discipline rules (`WORKFLOW.md` §2) apply at write time:
55
+ - No emoji anywhere in the file.
56
+ - `Context` is the business-context-first section — *forces* and *problem* before the *decision*.
57
+ - One scope: one decision per ADR.
58
+ - No speculation. `Decision` is a directive; rejected paths go in `Alternatives Considered`.
53
59
  </output_contract>
@@ -85,4 +85,11 @@ Currently-binding decisions. Link each to `doc/adr/`.
85
85
 
86
86
  <output_contract>
87
87
  A single `ARCHITECTURE.md` at the repo root. Every line locks a binding pattern. ADR candidates flagged in the response, not written. In audit mode: drift list only, no file written.
88
+
89
+ `ARCHITECTURE.md` is a narrative document, so the Documentation Discipline rules in `WORKFLOW.md` §2 apply at write time:
90
+ - No emoji anywhere in the file.
91
+ - No dates, version stamps, `DRAFT` markers, or changelog blocks. Architecture is the current binding pattern; lifecycle lives in ADRs and git history.
92
+ - `Overview` is the business-context-first paragraph — *what the system does* and *what would break without it* before layers and patterns.
93
+ - One scope: system-level patterns and boundaries. Per-decision rationale lives in ADRs; per-project operations live in `AGENTS.md`. Link, do not copy.
94
+ - No speculation. If a layer has no observed signal, write `<TODO: not yet wired>` once; do not propose patterns absent from the code.
88
95
  </output_contract>
@@ -29,10 +29,21 @@ ADR drift (if `doc/adr/` exists):
29
29
  - Status field — every ADR has one of `proposed | accepted | deprecated | superseded by ADR-NNNN`.
30
30
  - Superseded chains — every "superseded by ADR-NNNN" target exists.
31
31
 
32
+ Documentation discipline drift (`WORKFLOW.md` §2 / ADR-0008). Audit narrative documents — `README.md`, `AGENTS.md` / `CLAUDE.md`, `ARCHITECTURE.md`, `DESIGN.md`, any prose page under `doc/` that is not a decision-record artifact under `doc/adr/` or `doc/tasks/`:
33
+ - Emoji — any present? Rule 3 forbids emoji anywhere (docs, code, comments, commits, skill outputs).
34
+ - Dates / version stamps / `DRAFT` markers / changelog blocks in narrative documents — Rule 2 forbids these. Decision-record artifacts under `doc/adr/` and `doc/tasks/` are exempt.
35
+ - Business context first — does the first paragraph answer *why* the document exists, before *what* and *how*? Rule 4.
36
+ - Scope duplication — does the document copy material that is canonically owned by another file? Rule 5 requires linking, not copying.
37
+ - Speculation — phrases like "we might", "in the future", "could be added", or roadmaps without an ADR / task reference. Rule 1 forbids unfounded plans.
38
+
39
+ Source code (sample, not exhaustive — flag findings, not every match):
40
+ - Orphan `TODO` / `FIXME` — Rule 7. A reference to a GitHub Issue or a `doc/tasks/NNNN-*.md` task file makes it not orphan.
41
+ - Commented-out code blocks — Rule 7. Removed code lives in git history.
42
+
32
43
  Step 3 — output. One line per finding, formatted:
33
44
  `[file or section]: spec says X, code says Y. Suggested resolution: change spec / change code / discuss.`
34
45
 
35
- Group by artifact. If a category has no drift, print one line: `AGENTS.md — no drift.` etc. If an audited artifact does not exist, say so explicitly rather than reporting zero findings.
46
+ Group by artifact. If a category has no drift, print one line: `AGENTS.md — no drift.` etc. If an audited artifact does not exist, say so explicitly rather than reporting zero findings. The Documentation discipline drift category groups findings under `Documentation discipline — <category>: ...`.
36
47
 
37
48
  If something the user says contradicts what the code shows, surface the conflict. Don't silently trust the user; don't silently trust the code.
38
49
  </instructions>
@@ -135,4 +135,11 @@ Real traps. Each one should map to an incident or to specific code.
135
135
 
136
136
  <output_contract>
137
137
  A single `AGENTS.md` at the repo root, ≤150 lines, every line operational. No "External Resources" section. No appended Universal Agent Behavior block — that lives in the `agentic-philosophy` skill. No meta-prose explaining gaps. In audit mode: a drift list, no file written.
138
+
139
+ `AGENTS.md` is a narrative document, so the Documentation Discipline rules in `WORKFLOW.md` §2 apply at write time:
140
+ - No emoji anywhere in the file.
141
+ - No dates, version stamps, `DRAFT` markers, or changelog blocks. Stack versions are facts (`Node ≥18`); release timelines are not.
142
+ - `Project Overview` is the business-context-first paragraph — *why* the project exists before *what* it does.
143
+ - One scope: this file is the operational guide for agents. Do not duplicate `ARCHITECTURE.md` patterns or ADR rationale here; link instead.
144
+ - No speculation. If a section has no signal, write `<TODO: not yet wired>` once; do not narrate "this could be added later".
138
145
  </output_contract>
@@ -45,4 +45,11 @@ If the source has tokens DESIGN.md doesn't document, list those as additions. If
45
45
 
46
46
  <output_contract>
47
47
  A single `DESIGN.md` at the repo root. YAML frontmatter uses W3C `$value`/`$type` shape. Markdown body has one section per token group present in the source. No invented tokens. No "External Resources" section. In audit mode: a drift list, no file written.
48
+
49
+ `DESIGN.md` is a narrative document, so the Documentation Discipline rules in `WORKFLOW.md` §2 apply at write time:
50
+ - No emoji anywhere — including do's and don'ts.
51
+ - No dates, version stamps, `DRAFT` markers, or changelog blocks. Token revisions live in git history; DESIGN.md is the current visual contract.
52
+ - The Markdown body opens with the *why* of each token group — the visual constraint or product principle — before listing rules.
53
+ - One scope: visual contract. Component anatomy and interaction patterns live elsewhere; link, do not copy.
54
+ - No speculation. If a group has no source token, mark `<TODO: not yet wired>` and move on.
48
55
  </output_contract>
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: agentic-philosophy
3
- description: Universal agent behavior — think before coding, ground in real patterns, prefer simplicity, make surgical changes, define verifiable goals, verify before claiming done. Auto-invokes on non-trivial changes, refactors, debugging, "think before coding", "ground before coding", "verify done", "before implementing", or whenever the task is ambiguous enough that guardrails matter.
3
+ description: Universal agent behavior and documentation discipline — think before coding, ground in real patterns, prefer simplicity, make surgical changes, define verifiable goals, verify before claiming done, and write documentation that captures only definitions and decisions. Auto-invokes on non-trivial changes, refactors, debugging, "think before coding", "ground before coding", "verify done", "before implementing", on documentation work — "writing docs", "writing readme", "writing architecture", "writing adr", "writing task", "audit docs" — or whenever the task is ambiguous enough that guardrails matter.
4
4
  ---
5
5
 
6
6
  <background_information>
7
- Six behaviors apply to every non-trivial change. Bias toward caution over speed; for trivial diffs, use judgment.
7
+ Six behaviors apply to every non-trivial change. Bias toward caution over speed; for trivial diffs, use judgment. A separate Documentation Discipline block applies to every document the agent writes.
8
8
  </background_information>
9
9
 
10
10
  <instructions>
@@ -19,6 +19,19 @@ Six behaviors apply to every non-trivial change. Bias toward caution over speed;
19
19
  **Goal-Driven Execution.** Define success criteria. Loop until verified. Transform vague tasks into verifiable goals ("Add validation" → "Write tests for invalid inputs, then make them pass"). Before modifying a file, list which tests cover it; run, modify, run; if none, write one first. For multi-step tasks, state a brief plan with a verify step per item.
20
20
 
21
21
  **Verify Before Claiming Done.** Type-check and tests verify code, not feature. For UI/runtime changes, exercise in a browser. Can't verify? Say so — don't claim success. Never bypass gates (`--no-verify`, skipped hooks, deleted failing tests).
22
+
23
+ **Documentation Discipline.** Every document the agent writes obeys eight rules. Canonical source: `WORKFLOW.md` §2. Decision: ADR-0008.
24
+
25
+ 1. **Definitions and decisions only.** What is true now, plus the decisions that brought it there. No speculation, no history, no unfounded plans. A deferred decision is in scope when *recorded* — an accepted ADR or a task file is fundamentação; "we might do X later" without a record is cut.
26
+ 2. **No dates, version stamps, `DRAFT` markers, or changelogs in narrative documents.** Applies to `README.md`, `AGENTS.md` / `CLAUDE.md`, `ARCHITECTURE.md`, `DESIGN.md`, specs, prose pages. **Decision-record artifacts are exempt** — ADRs under `doc/adr/` keep `**Status:**` / `**Date:**`; tasks under `doc/tasks/` keep `**Created:**` and the append-only dated `Notes` log. Outside those, use git history.
27
+ 3. **No emoji anywhere.** Not in docs, code, source comments, commit messages, PR bodies, or skill outputs. Severity and status use words; structural cues use Markdown.
28
+ 4. **Business context first.** Open with *why* — the problem, the constraint, the user — before *what* and *how*. First paragraph answers "what would break if this document didn't exist".
29
+ 5. **One scope per document. No duplication.** Link instead of copying. The canonical location owns the content; everywhere else references it.
30
+ 6. **Code is the primary documentation of behavior.** Comments justify *why* a non-obvious choice was made — never restate *what*. If the comment explains *what*, rename or refactor.
31
+ 7. **No commented-out code; no orphan `TODO` / `FIXME` in source.** Every deferred item references a tracked work item — a GitHub Issue, or a per-task file under `doc/tasks/NNNN-*.md`. The trace must be addressable from the source line.
32
+ 8. **Tests are living documentation of behavior.** Test names and assertions read as the spec they enforce. Spec changes drive test changes; never the reverse.
33
+
34
+ Walk this list before declaring any documentation task done.
22
35
  </instructions>
23
36
 
24
37
  <output_contract>
@@ -25,7 +25,7 @@ Step 1 — assemble the handoff. The fresh session will get only what you print
25
25
  - Relevant task file under `doc/tasks/` — if the diff or recent commit messages reference `Task NNNN`, read its Acceptance Criteria and Plan.
26
26
  - Recent commit messages for the range (`git log <range> --format=%B`).
27
27
 
28
- Step 2 — print the handoff inline. Format the message exactly as below so the user can copy it as a single block. Do not summarize what you think the diff does.
28
+ Step 2 — write the handoff to disk. Save the assembled handoff at `.agentic/reviews/<ISO-timestamp>-<scope-slug>.md` at the repo root. Create the directory if missing. The slug encodes the scope (`branch-vs-main`, `pr-42`, `commit-abc1234`, `working-tree`). The file body is exactly the block below — no prose summary of what you think the diff does, the reviewer reads the diff itself.
29
29
 
30
30
  ```
31
31
  === AGENTIC-REVIEW HANDOFF ===
@@ -41,7 +41,7 @@ Review focus, in priority order:
41
41
 
42
42
  Skip formatting, naming opinions, stylistic preferences. Skip praise.
43
43
 
44
- Output: group findings by severity (Blocker / Concern / Note). Each finding one line: `file:line: <emoji> <severity>: <problem>. <fix>.` Use 🚨 / ⚠️ / ℹ️.
44
+ Output: group findings by severity (Blocker / Concern / Note). Each finding one line: `file:line: <severity>: <problem>. <fix>.` Severity is the literal word `Blocker`, `Concern`, or `Note` — no emoji.
45
45
 
46
46
  End with a one-line bottom-line: "Ship as-is", "Ship with the Concerns logged as follow-up tasks", or "Don't ship until Blockers resolved". Do NOT synthesize an "approve" verdict.
47
47
 
@@ -54,15 +54,24 @@ End with a one-line bottom-line: "Ship as-is", "Ship with the Concerns logged as
54
54
  === END HANDOFF ===
55
55
  ```
56
56
 
57
- Step 3 instruct the user. After printing the handoff, output exactly this and stop:
57
+ Advise the user to add `.agentic/reviews/` to their `.gitignore` if it is not already — handoffs are ephemeral per-review artifacts, not committed history.
58
+
59
+ Step 3 — instruct the user. After writing the handoff, output exactly this and stop:
58
60
 
59
61
  ```
60
- Copy the handoff above. Run `/clear` to drop the current context. Paste the handoff into the empty session. Codex will produce the structured findings.
62
+ Handoff saved at <path>.
63
+
64
+ Load it into a fresh Codex session:
65
+
66
+ cat <path> | pbcopy # macOS
67
+ xclip -selection clipboard < <path> # Linux
68
+
69
+ Then in Codex: run `/clear`, paste from the clipboard, send. Codex will produce the structured findings.
61
70
  ```
62
71
 
63
72
  Do not proceed past this point in the current session.
64
73
  </instructions>
65
74
 
66
75
  <output_contract>
67
- One inline message containing the assembled handoff (review instructions + diff + spec slice), followed by a short instruction telling the user to /clear and paste. No file is written. The current session does not produce findings — the fresh session does, after the user re-loads the handoff. This honors WORKFLOW §10 by enforcing context isolation through `/clear` rather than via a subagent primitive Codex lacks.
76
+ A handoff file at `.agentic/reviews/<ISO-timestamp>-<scope-slug>.md`, plus a short instruction telling the user how to load it into a fresh Codex session via `/clear` and paste. The current session does not produce findings — the fresh session does, after the user re-loads the handoff. This honors WORKFLOW §10 by enforcing context isolation through `/clear` rather than via a subagent primitive Codex lacks; the persisted file replaces the chat-scroll copy step that previously made the round-trip fragile.
68
77
  </output_contract>
@@ -79,4 +79,11 @@ All Acceptance Criteria checked, plus:
79
79
 
80
80
  <output_contract>
81
81
  A single new file at `doc/tasks/<NNNN>-<short-slug>.md`. Status proposed. Notes empty. No existing tasks modified. No invented values.
82
+
83
+ Task files are decision-record artifacts and are exempt from the no-dates rule (Documentation Discipline §2): `**Created:**` and the dated `Notes` log are required by design. Remaining Documentation Discipline rules (`WORKFLOW.md` §2) apply at write time:
84
+ - No emoji anywhere in the file.
85
+ - `Context` is the business-context-first section — *why this task exists* before *Acceptance Criteria*.
86
+ - One scope: one task per file.
87
+ - No speculation. Acceptance criteria must be measurable; do not list aspirational items.
88
+ - `Notes` is append-only and dated per entry — that is the auditability primitive, not a violation of Rule 2.
82
89
  </output_contract>