dorfl 0.3.2 → 0.5.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/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +108 -0
- package/dist/cli.js.map +1 -1
- package/dist/prd-to-spec.d.ts +3 -52
- package/dist/prd-to-spec.d.ts.map +1 -1
- package/dist/prd-to-spec.js +12 -94
- package/dist/prd-to-spec.js.map +1 -1
- package/dist/protocol/TASKING-PROTOCOL.md +22 -1
- package/dist/resync-protocol.d.ts +73 -0
- package/dist/resync-protocol.d.ts.map +1 -0
- package/dist/resync-protocol.js +114 -0
- package/dist/resync-protocol.js.map +1 -0
- package/dist/skills/setup/SKILL.md +1 -1
- package/dist/skills/setup/protocol/TASKING-PROTOCOL.md +22 -1
- package/dist/skills/to-spec/SKILL.md +2 -0
- package/dist/skills/to-task/SKILL.md +2 -2
- package/dist/tasking.d.ts.map +1 -1
- package/dist/tasking.js +15 -0
- package/dist/tasking.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +157 -0
- package/src/prd-to-spec.ts +18 -140
- package/src/resync-protocol.ts +162 -0
- package/src/tasking.ts +15 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
copyFileSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
} from 'node:fs';
|
|
8
|
+
import {join, relative, dirname} from 'node:path';
|
|
9
|
+
import {resolveProtocolDoc} from './prompt.js';
|
|
10
|
+
import {WORK_ROOT} from './work-layout.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The **protocol re-sync** primitive: copy the package's canonical
|
|
14
|
+
* `work/protocol/*` docs into a target repo's `work/protocol/` and bump
|
|
15
|
+
* `work/protocol/VERSION`. This is the deterministic slice of what the `setup`
|
|
16
|
+
* skill does — the SINGLE source of truth for "make this repo carry the current
|
|
17
|
+
* protocol contract".
|
|
18
|
+
*
|
|
19
|
+
* Two verbs drive it:
|
|
20
|
+
* - `dorfl sync` — the standalone "get the latest protocol" command (a repo
|
|
21
|
+
* that adopted an OLD protocol picks up the new docs in one command);
|
|
22
|
+
* - `dorfl prd-to-spec` — the vocabulary-migration engine, which re-syncs the
|
|
23
|
+
* contract FIRST (before converting the repo's data) so the migrated repo
|
|
24
|
+
* lands on the new `spec` contract.
|
|
25
|
+
*
|
|
26
|
+
* It was lifted out of `prd-to-spec.ts` (where it began life) into this shared
|
|
27
|
+
* module so both verbs import ONE implementation; `prd-to-spec.ts` re-exports
|
|
28
|
+
* the symbols for backward compatibility.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The FULL set of protocol docs `setup` propagates into a repo's
|
|
33
|
+
* `work/protocol/`. The re-sync copies EACH from the package's canonical source
|
|
34
|
+
* (via {@link resolveProtocolDoc}) so a set-up repo carries the current
|
|
35
|
+
* contract. Kept in lockstep with `skills/setup/protocol/` (the source of truth)
|
|
36
|
+
* and the `vendor-protocol` build step (which vendors these into `dist/protocol/`
|
|
37
|
+
* so the published CLI is self-contained).
|
|
38
|
+
*/
|
|
39
|
+
export const PROTOCOL_DOCS: readonly string[] = [
|
|
40
|
+
'WORK-CONTRACT.md',
|
|
41
|
+
'CLAIM-PROTOCOL.md',
|
|
42
|
+
'REVIEW-PROTOCOL.md',
|
|
43
|
+
'SURFACE-PROTOCOL.md',
|
|
44
|
+
'TASKING-PROTOCOL.md',
|
|
45
|
+
'ADR-FORMAT.md',
|
|
46
|
+
'task-template.md',
|
|
47
|
+
'spec-template.md',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/** One protocol doc the re-sync copied (or WOULD copy under `--dry-run`). */
|
|
51
|
+
export interface ResyncedDoc {
|
|
52
|
+
/** The doc basename (e.g. `'WORK-CONTRACT.md'`). */
|
|
53
|
+
name: string;
|
|
54
|
+
/** Repo-relative destination (`work/protocol/<name>`). */
|
|
55
|
+
dest: string;
|
|
56
|
+
/** True when the target already had byte-identical content (a no-op copy). */
|
|
57
|
+
unchanged: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* True when the doc's SOURCE could not be resolved (the package/dev copy is
|
|
60
|
+
* missing), so NOTHING was copied to `dest`. A skipped doc is NEVER counted as
|
|
61
|
+
* a change (it must not bump `VERSION` — the latent bug this field guards) and
|
|
62
|
+
* is surfaced LOUDLY by the caller. Distinct from `unchanged` (which means the
|
|
63
|
+
* source WAS resolved and matched the dest byte-for-byte).
|
|
64
|
+
*/
|
|
65
|
+
skipped: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What the protocol re-sync did (or would do). */
|
|
69
|
+
export interface ResyncResult {
|
|
70
|
+
docs: ResyncedDoc[];
|
|
71
|
+
/** Repo-relative `work/protocol/VERSION` path (written unless dry-run). */
|
|
72
|
+
versionPath: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Re-sync the target repo's `work/protocol/*` from the package's canonical
|
|
77
|
+
* source (the deterministic part of the `setup` skill — copy the docs verbatim +
|
|
78
|
+
* bump `VERSION`). Resolves each doc's SOURCE via {@link resolveProtocolDoc} with
|
|
79
|
+
* NO `cwd`, so it reads the package-vendored (`dist/protocol/`) or dev-source
|
|
80
|
+
* (`skills/setup/protocol/`) copy — NEVER the target repo's own (old) copy. This
|
|
81
|
+
* is how a repo picks up the current contract. Idempotent: a doc already
|
|
82
|
+
* byte-identical is reported `unchanged`. A doc whose SOURCE cannot be resolved
|
|
83
|
+
* is reported `skipped` (NOT copied, NEVER a VERSION-bump) rather than silently
|
|
84
|
+
* bumping VERSION with nothing copied.
|
|
85
|
+
*
|
|
86
|
+
* `dryRun` reports what WOULD be copied without writing. `resolveDoc` overrides
|
|
87
|
+
* how a doc's source path is resolved (defaults to {@link resolveProtocolDoc});
|
|
88
|
+
* it exists so a test can force a non-resolvable source (a path that does not
|
|
89
|
+
* exist) to exercise the skip path. `sourceCommit` is stamped into the written
|
|
90
|
+
* `VERSION` provenance line so a reader can tell which verb wrote it.
|
|
91
|
+
*/
|
|
92
|
+
export function resyncProtocol(
|
|
93
|
+
repoPath: string,
|
|
94
|
+
options: {
|
|
95
|
+
dryRun?: boolean;
|
|
96
|
+
resolveDoc?: (name: string) => string;
|
|
97
|
+
sourceCommit?: string;
|
|
98
|
+
} = {},
|
|
99
|
+
): ResyncResult {
|
|
100
|
+
const protocolDir = join(repoPath, WORK_ROOT, 'protocol');
|
|
101
|
+
const resolveDoc = options.resolveDoc ?? ((name) => resolveProtocolDoc(name));
|
|
102
|
+
const docs: ResyncedDoc[] = [];
|
|
103
|
+
for (const name of PROTOCOL_DOCS) {
|
|
104
|
+
// Resolve the SOURCE (package/dev), never the target's adopted copy.
|
|
105
|
+
const source = resolveDoc(name);
|
|
106
|
+
const destAbs = join(protocolDir, name);
|
|
107
|
+
const destRel = relative(repoPath, destAbs);
|
|
108
|
+
const sourceExists = existsSync(source);
|
|
109
|
+
if (!sourceExists) {
|
|
110
|
+
// The source doc could not be resolved: copy NOTHING and record the doc as
|
|
111
|
+
// SKIPPED (never a change). This is the latent-bug fix: a non-resolvable
|
|
112
|
+
// source must not count as `changed` and bump `VERSION` while copying no
|
|
113
|
+
// file. `unchanged: false` here is honest (the dest was not made to match a
|
|
114
|
+
// source), but `skipped: true` keeps it OUT of the VERSION-bump tally.
|
|
115
|
+
docs.push({name, dest: destRel, unchanged: false, skipped: true});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Source resolved: a doc is CHANGED (VERSION-bump-worthy) only when the dest
|
|
119
|
+
// is absent OR its content differs from the source. An identical dest is a
|
|
120
|
+
// no-op copy (`unchanged`), so an already-synced re-run stays a true no-op.
|
|
121
|
+
const unchanged =
|
|
122
|
+
existsSync(destAbs) &&
|
|
123
|
+
readFileSync(source, 'utf8') === readFileSync(destAbs, 'utf8');
|
|
124
|
+
if (!options.dryRun) {
|
|
125
|
+
mkdirSync(dirname(destAbs), {recursive: true});
|
|
126
|
+
copyFileSync(source, destAbs);
|
|
127
|
+
}
|
|
128
|
+
docs.push({name, dest: destRel, unchanged, skipped: false});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const versionAbs = join(protocolDir, 'VERSION');
|
|
132
|
+
const versionRel = relative(repoPath, versionAbs);
|
|
133
|
+
// Bump VERSION only when a doc was ACTUALLY COPIED with new content, or when
|
|
134
|
+
// VERSION is absent AND at least one doc was genuinely synced (copied). A
|
|
135
|
+
// SKIPPED doc (unresolvable source, nothing copied) is NEVER a change —
|
|
136
|
+
// counting it would bump VERSION without copying a single doc (the latent bug),
|
|
137
|
+
// and the write-when-absent fallback must ALSO require a real copy so an
|
|
138
|
+
// all-skipped resync leaves no VERSION behind. A re-run on an already-synced
|
|
139
|
+
// repo must stay a true no-op — otherwise the (always-fresh) `synced-at`
|
|
140
|
+
// timestamp would dirty the tree on every idempotent re-run.
|
|
141
|
+
const anyDocChanged = docs.some((d) => !d.unchanged && !d.skipped);
|
|
142
|
+
const anyDocSynced = docs.some((d) => !d.skipped);
|
|
143
|
+
if (
|
|
144
|
+
!options.dryRun &&
|
|
145
|
+
(anyDocChanged || (anyDocSynced && !existsSync(versionAbs)))
|
|
146
|
+
) {
|
|
147
|
+
mkdirSync(dirname(versionAbs), {recursive: true});
|
|
148
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
149
|
+
writeFileSync(
|
|
150
|
+
versionAbs,
|
|
151
|
+
[
|
|
152
|
+
`protocol-version: ${today}`,
|
|
153
|
+
'synced-from: skills/setup/protocol/',
|
|
154
|
+
`synced-at: ${new Date().toISOString()}`,
|
|
155
|
+
`source-commit: ${options.sourceCommit ?? 'dorfl sync'}`,
|
|
156
|
+
'',
|
|
157
|
+
].join('\n'),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {docs, versionPath: versionRel};
|
|
162
|
+
}
|
package/src/tasking.ts
CHANGED
|
@@ -1361,6 +1361,21 @@ function buildTaskingSpec(slug: string, _spec: string | undefined): string {
|
|
|
1361
1361
|
`rather than guessing — or, if the whole decomposition is unclear, stop and`,
|
|
1362
1362
|
`route the spec to needs-attention with the questions.`,
|
|
1363
1363
|
``,
|
|
1364
|
+
`Task the spec ATOMICALLY or NOT AT ALL (\`TASKING-PROTOCOL.md\` §2a): run the`,
|
|
1365
|
+
`COMPLETENESS CHECK across the spec's user stories FIRST — a three-branch`,
|
|
1366
|
+
`decision. Every story becomes a task now, or NONE does; there is no "partially`,
|
|
1367
|
+
`tasked" state. (1) EVERY story build-taskable now (committed, answered, and you`,
|
|
1368
|
+
`know HOW to build it) — task the whole spec. (2) SOME stories gated / deferred /`,
|
|
1369
|
+
`unanswered (mixed confidence) — the spec is MIS-SCOPED: do NOT task the confident`,
|
|
1370
|
+
`subset; it must be SPLIT (a fully-taskable spec plus a separate spec for the`,
|
|
1371
|
+
`gated remainder). (3) The WHOLE thing too big / too uncertain to build-task even`,
|
|
1372
|
+
`after splitting (you do NOT yet know HOW to build it) — do NOT write FICTIONAL`,
|
|
1373
|
+
`build tasks; it must be REFRAMED as an EXPLORATION spec whose "done" is`,
|
|
1374
|
+
`confidence + a de-risked build plan (pin the seam, spike the risky part on one`,
|
|
1375
|
+
`case, resolve the questions, emit the plan). In BOTH the split and the reframe`,
|
|
1376
|
+
`case, emit NO tasks: stop and route the spec to needs-attention with the reason`,
|
|
1377
|
+
`(a human authors the split / exploration reframe).`,
|
|
1378
|
+
``,
|
|
1364
1379
|
`WRITE EVERY emitted task file under \`${STAGED_TASKS_DIR}/\` (the STAGING folder)`,
|
|
1365
1380
|
`— NEVER \`work/tasks/ready/\`. \`work/tasks/ready/\` is the agent-eligible POOL and`,
|
|
1366
1381
|
`the runner owns the runner/human-only promotion into it; the tasker's staging`,
|