pincer-workflow 0.5.0 → 0.7.0
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 +106 -19
- package/bin/pincer.js +17 -1
- package/package.json +3 -3
- package/template/.agents/skills/pincer-code/SKILL.md +88 -14
- package/template/.agents/skills/pincer-evaluate/SKILL.md +42 -13
- package/template/.agents/skills/pincer-narrow/SKILL.md +53 -12
- package/template/.agents/skills/pincer-plan/SKILL.md +12 -4
- package/template/.agents/skills/pincer-release/SKILL.md +18 -0
- package/template/.agents/skills/pincer-status/SKILL.md +29 -5
- package/template/.claude/commands/pincer-code.md +88 -14
- package/template/.claude/commands/pincer-evaluate.md +42 -13
- package/template/.claude/commands/pincer-narrow.md +53 -12
- package/template/.claude/commands/pincer-plan.md +12 -4
- package/template/.claude/commands/pincer-release.md +18 -0
- package/template/.claude/commands/pincer-status.md +29 -5
- package/template/.claude/hooks/hook-policy.cjs +13 -6
- package/template/.claude/references/prd-template.md +11 -4
- package/template/.codex/README.md +1 -1
- package/template/.github/prompts/pincer-code.prompt.md +88 -14
- package/template/.github/prompts/pincer-evaluate.prompt.md +42 -13
- package/template/.github/prompts/pincer-narrow.prompt.md +53 -12
- package/template/.github/prompts/pincer-plan.prompt.md +12 -4
- package/template/.github/prompts/pincer-release.prompt.md +18 -0
- package/template/.github/prompts/pincer-status.prompt.md +29 -5
- package/template/AGENTS.md +17 -1
- package/template/docs/dry-run-checklist.md +30 -3
- package/template/docs/release-checklist.md +3 -1
- package/template/docs/runtime-contracts.md +1428 -96
- package/template/scripts/pincer-evidence.cjs +9 -7
- package/template/scripts/pincer-runtime/adopt.cjs +132 -0
- package/template/scripts/pincer-runtime/agreement.cjs +240 -0
- package/template/scripts/pincer-runtime/authorization.cjs +167 -0
- package/template/scripts/pincer-runtime/changes.cjs +517 -0
- package/template/scripts/pincer-runtime/checks.cjs +48 -0
- package/template/scripts/pincer-runtime/coverage.cjs +361 -0
- package/template/scripts/pincer-runtime/dispositions.cjs +95 -0
- package/template/scripts/pincer-runtime/evidence.cjs +303 -18
- package/template/scripts/pincer-runtime/gates.cjs +73 -0
- package/template/scripts/pincer-runtime/identity.cjs +21 -4
- package/template/scripts/pincer-runtime/impact.cjs +177 -0
- package/template/scripts/pincer-runtime/io.cjs +41 -0
- package/template/scripts/pincer-runtime/lifecycle.cjs +34 -12
- package/template/scripts/pincer-runtime/locator.cjs +158 -0
- package/template/scripts/pincer-runtime/migrate.cjs +140 -63
- package/template/scripts/pincer-runtime/parse.cjs +20 -1
- package/template/scripts/pincer-runtime/phases.cjs +245 -0
- package/template/scripts/pincer-runtime/readiness.cjs +15 -2
- package/template/scripts/pincer-runtime/requirements.cjs +255 -0
- package/template/scripts/pincer-runtime/resume.cjs +273 -0
- package/template/scripts/pincer-runtime/routing.cjs +54 -0
- package/template/scripts/pincer-runtime/runner.cjs +24 -6
- package/template/scripts/pincer-runtime/scaffold.cjs +254 -0
- package/template/scripts/pincer-runtime/state.cjs +29 -7
- package/template/scripts/pincer-runtime/status.cjs +178 -22
- package/template/scripts/pincer-runtime/transaction.cjs +200 -0
- package/template/scripts/pincer-runtime/transitions.cjs +134 -0
- package/template/scripts/pincer-runtime.cjs +412 -76
- package/template/scripts/pincer-status.sh +1 -1
- package/template/scripts/pincer-ticket.sh +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// PINCER runtime — change lifecycle transitions (docs/runtime-contracts.md,
|
|
3
|
+
// "Lifecycle"). planned → active → paused/completed → …, cancelled and
|
|
4
|
+
// superseded as terminal states, each transition one transaction that checks the
|
|
5
|
+
// whole precondition set under the lock, appends exactly one event and rewrites
|
|
6
|
+
// the projection; an invalid transition writes nothing; requesting the state a
|
|
7
|
+
// record already has writes no event. Running attempts are never terminated
|
|
8
|
+
// here: pause, complete, cancel and supersede refuse while one runs.
|
|
9
|
+
const transaction = require('./transaction.cjs');
|
|
10
|
+
const agreement = require('./agreement.cjs');
|
|
11
|
+
const { inlineSecretLine } = require('./sanitize.cjs');
|
|
12
|
+
|
|
13
|
+
const OPS = ['activate', 'pause', 'resume', 'complete', 'reopen', 'cancel', 'supersede'];
|
|
14
|
+
const NEEDS_SELECTION = ['activate', 'pause', 'resume', 'complete', 'reopen'];
|
|
15
|
+
const NEEDS_IDLE = ['pause', 'complete', 'cancel', 'supersede'];
|
|
16
|
+
const ACTIVATION = ['activate', 'resume', 'reopen'];
|
|
17
|
+
const DECISION_ID = /^D-[0-9]{2,6}$/;
|
|
18
|
+
|
|
19
|
+
function text(value, name, options) {
|
|
20
|
+
const v = transaction.boundedText(value, name, options);
|
|
21
|
+
if (v !== null && inlineSecretLine([v])) transaction.refuse('INPUT_INVALID', `--${name} assigns a secret-like literal; reference secrets by name, never by value`);
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
// The operations the table permits from a state (for refusal messages).
|
|
25
|
+
function permitted(changes, state) {
|
|
26
|
+
return OPS.filter(op => changes.LIFECYCLE_KINDS[op][0].includes(state));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// transition(root, id, op, opts): opts = { reason, note, decision, with, expect, hooks }.
|
|
30
|
+
// Returns { action: 'transitioned' | 'unchanged', record, from, to, event } or { code, problem }.
|
|
31
|
+
function transition(root, id, op, opts = {}) {
|
|
32
|
+
const changes = require('./changes.cjs');
|
|
33
|
+
const authorization = require('./authorization.cjs');
|
|
34
|
+
const statusModule = require('./status.cjs');
|
|
35
|
+
if (!OPS.includes(op)) return { code: 'INPUT_INVALID', problem: `unknown lifecycle operation ${op}` };
|
|
36
|
+
const [froms, to] = changes.LIFECYCLE_KINDS[op];
|
|
37
|
+
try {
|
|
38
|
+
const out = transaction.run(root, { command: `change ${op} ${id}`, hooks: opts.hooks || null }, ctx => {
|
|
39
|
+
const resolved = changes.resolveSelected(root, { change: id });
|
|
40
|
+
if (resolved.code) ctx.refuse(resolved.code, resolved.problem);
|
|
41
|
+
if (opts.expect !== undefined && opts.expect !== null) ctx.expect(resolved.file, 'sequence', opts.expect);
|
|
42
|
+
const record = resolved.record;
|
|
43
|
+
const from = record.lifecycle.state;
|
|
44
|
+
// Idempotence: the requested state is already the state (same replacement for supersede).
|
|
45
|
+
if (from === to && (op !== 'supersede' || record.lifecycle.superseded_by === opts.with)) return { action: 'unchanged', record, from, to };
|
|
46
|
+
if (changes.TERMINAL.includes(from)) ctx.refuse('LIFECYCLE_BLOCKED', `change ${id} is ${from}${record.lifecycle.superseded_by ? ` by ${record.lifecycle.superseded_by}` : ''}; its history is inspectable (change show ${id}) but it cannot be ${op}d — register a new change and reference this record`);
|
|
47
|
+
if (!froms.includes(from)) ctx.refuse('LIFECYCLE_BLOCKED', `change ${id} is ${from}; ${op} applies to ${froms.join(' or ')} changes — permitted now: ${permitted(changes, from).map(o => `change ${o}`).join(', ')}`);
|
|
48
|
+
if (NEEDS_SELECTION.includes(op)) {
|
|
49
|
+
if (!resolved.selection) ctx.refuse('SELECTION_REQUIRED', `${op} needs the change selected in this worktree: node scripts/pincer-runtime.cjs change select ${id}`);
|
|
50
|
+
if (resolved.selection.change !== id) ctx.refuse('WRONG_CHANGE', `the selected change is ${resolved.selection.change}, not ${id}; select it first: node scripts/pincer-runtime.cjs change select ${id}`);
|
|
51
|
+
}
|
|
52
|
+
const reason = ['pause', 'reopen', 'cancel'].includes(op) ? text(opts.reason, 'reason') : null;
|
|
53
|
+
const note = op === 'pause' ? text(opts.note, 'note', { required: false }) : null;
|
|
54
|
+
let decision = null;
|
|
55
|
+
if (['cancel', 'supersede'].includes(op)) {
|
|
56
|
+
if (typeof opts.decision !== 'string' || !DECISION_ID.test(opts.decision)) ctx.refuse('INPUT_INVALID', `${op} requires --decision D-NN, the user's recorded decision (change decide ${id} --summary … then --resolve)`);
|
|
57
|
+
const d = record.decisions.find(x => x.id === opts.decision);
|
|
58
|
+
if (!d) ctx.refuse('INPUT_INVALID', `--decision ${opts.decision} is not a decision of change ${id} (recorded: ${record.decisions.map(x => x.id).join(', ') || 'none'})`);
|
|
59
|
+
if (d.status !== 'resolved') ctx.refuse('DECISION_REQUIRED', `decision ${d.id} is still open; record the user's decision first: node scripts/pincer-runtime.cjs change decide ${id} --resolve ${d.id} --reference <text> --excerpt <text>`);
|
|
60
|
+
decision = d.id;
|
|
61
|
+
}
|
|
62
|
+
let replacement = null;
|
|
63
|
+
if (op === 'supersede') {
|
|
64
|
+
const w = opts.with;
|
|
65
|
+
if (typeof w !== 'string' || !changes.CHANGE_ID.test(w)) ctx.refuse('INPUT_INVALID', 'supersede requires --with <replacement change id>');
|
|
66
|
+
if (w === id) ctx.refuse('LIFECYCLE_BLOCKED', `change ${id} cannot supersede itself`);
|
|
67
|
+
if (!resolved.loaded.records.has(w)) ctx.refuse('INPUT_INVALID', `replacement change "${w}" is not a retained record (retained: ${[...resolved.loaded.records.keys()].join(', ')}); register it first`);
|
|
68
|
+
// The replacement must not be (transitively) superseded by this record.
|
|
69
|
+
let cursor = resolved.loaded.records.get(w).record.lifecycle.superseded_by;
|
|
70
|
+
const seen = new Set([w]);
|
|
71
|
+
while (cursor) {
|
|
72
|
+
if (cursor === id) ctx.refuse('LIFECYCLE_BLOCKED', `change "${w}" is already superseded by ${[...seen].join(' → ')} → ${id}; superseding ${id} with it would form a cycle`);
|
|
73
|
+
if (seen.has(cursor)) break;
|
|
74
|
+
seen.add(cursor);
|
|
75
|
+
const next = resolved.loaded.records.get(cursor);
|
|
76
|
+
cursor = next ? next.record.lifecycle.superseded_by : null;
|
|
77
|
+
}
|
|
78
|
+
replacement = w;
|
|
79
|
+
}
|
|
80
|
+
if (NEEDS_IDLE.includes(op)) ctx.idle(id);
|
|
81
|
+
let authorized = null, agreementId = null, verdict = null;
|
|
82
|
+
if (ACTIVATION.includes(op) || op === 'complete') {
|
|
83
|
+
const v = changes.view(root, record);
|
|
84
|
+
if (v.problems.length) ctx.refuse(v.problems[0].code, v.problems[0].detail);
|
|
85
|
+
const computed = agreement.compute(root, record);
|
|
86
|
+
if (computed.code) ctx.refuse(computed.code, computed.problem);
|
|
87
|
+
verdict = authorization.verdict(root, record, computed);
|
|
88
|
+
if (verdict.verdict !== 'current') ctx.refuse(verdict.verdict, verdict.detail);
|
|
89
|
+
authorized = verdict.authorized.id; agreementId = verdict.authorized.agreement;
|
|
90
|
+
if (ACTIVATION.includes(op)) {
|
|
91
|
+
for (const [otherId, e] of resolved.loaded.records) {
|
|
92
|
+
if (otherId !== id && e.record.lifecycle.state === 'active') ctx.refuse('LIFECYCLE_BLOCKED', `change ${otherId} is active in this tree; pause or complete it before activating ${id} (at most one active change)`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (op === 'complete') {
|
|
97
|
+
const st = statusModule.render(root, { change: id });
|
|
98
|
+
const g = st.gathered;
|
|
99
|
+
if (!g || !g.tickets) ctx.refuse('INPUT_INVALID', st.text.trim().split('\n').find(l => l.startsWith('WARN')) || 'the change cannot be inspected');
|
|
100
|
+
if (g.unresolved > 0) ctx.refuse('INPUT_INVALID', 'a ticket has an unresolved PRD association; repair it before completing');
|
|
101
|
+
if (!g.tickets.length) ctx.refuse('LIFECYCLE_BLOCKED', `change ${id} has no tickets; a change completes only with a verified breakdown`);
|
|
102
|
+
// Strict coverage (docs/runtime-contracts.md, "Phase-specific coverage"): structural
|
|
103
|
+
// completeness — every scenario linked or authorized as not delivered, nothing
|
|
104
|
+
// missing from the baseline — precedes the ticket readiness gate below; a
|
|
105
|
+
// candidate is never demanded here.
|
|
106
|
+
if (changes.isStrict(record)) {
|
|
107
|
+
const report = require('./phases.cjs').compute(root, record, { gathered: g, verdict });
|
|
108
|
+
const blocker = require('./phases.cjs').firstBlocker(report, 'structure');
|
|
109
|
+
if (blocker) ctx.refuse(blocker.code, `${blocker.detail} — complete needs structural coverage: every scenario linked or dispositioned, every ticket classified, every disposition authorized`);
|
|
110
|
+
}
|
|
111
|
+
for (const t of g.tickets) {
|
|
112
|
+
const tid = t.fields.ticket;
|
|
113
|
+
if (t.fields.status !== 'done') ctx.refuse('LIFECYCLE_BLOCKED', `${tid} is ${t.fields.status}, not done; finish every ticket before completing ${id}`);
|
|
114
|
+
const r = g.computeReadiness(t);
|
|
115
|
+
if (!r.ready) ctx.refuse(r.reasons[0].code, `${tid}: ${r.reasons[0].detail} — ${r.reasons[0].next}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const sequence = record.sequence + 1;
|
|
119
|
+
const event = { sequence, kind: op, from, to, at: ctx.now, reason, agreement: agreementId, authorization: authorized, decision, replacement, note };
|
|
120
|
+
record.events.push(event);
|
|
121
|
+
record.sequence = sequence;
|
|
122
|
+
record.lifecycle = { state: to, since: ctx.now, reason, note, superseded_by: replacement };
|
|
123
|
+
ctx.write(resolved.file, record);
|
|
124
|
+
return { action: 'transitioned', record, from, to, event };
|
|
125
|
+
});
|
|
126
|
+
return out.result;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.refusal) return { code: error.code, problem: error.message };
|
|
129
|
+
if (error.code === 'STATE_BUSY') return { code: 'STATE_BUSY', problem: error.message };
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { OPS, transition, permitted };
|