@khoavu882/doflow 2.0.0 → 2.0.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 +4 -5
- package/bin/doflow.js +1 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +21 -1
- package/core/harnesses/claude/settings/settings.json +8 -9
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +21 -1
- package/core/harnesses/copilot/hooks/doflow-lib.sh +4 -3
- package/core/harnesses/copilot/hooks/doflow.json +0 -6
- package/core/harnesses/kiro/agents/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +7 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +7 -3
- package/core/harnesses/pi/extensions/doflow-guard.js +3 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -1
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +35 -25
- package/core/harnesses/shared/hooks/bridge/envelope.jq +1 -1
- package/core/harnesses/shared/hooks/bridge/respond.sh +6 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +1 -1
- package/core/harnesses/shared/hooks/policies/compact-text.sh +8 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +23 -1
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +4 -6
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +20 -21
- package/core/harnesses/shared/hooks/policies/stop-check.sh +7 -6
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +26 -2
- package/core/harnesses/shared/hooks/stream-hook-runner.js +11 -12
- package/core/registry/contracts.json +1 -1
- package/core/registry/contribution-points.json +5 -2
- package/core/registry/gates.json +5 -5
- package/core/registry/harnesses.json +9 -9
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +176 -503
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/packages/doflow/doflow-package.json +2 -2
- package/core/registry/rules.json +8 -8
- package/core/registry/settings.json +12 -16
- package/core/registry/storage.json +9 -2
- package/core/registry/telemetry.json +1 -1
- package/core/registry/verification.json +0 -5
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +7 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +4 -4
- package/core/shared/guidance/references/ORCHESTRATION.md +2 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +2 -2
- package/core/shared/scripts/doflow/bash/do-git-state.sh +12 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +86 -33
- package/core/shared/scripts/doflow/bash/do-review-package.sh +36 -2
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +1 -1
- package/core/shared/scripts/doflow/bin/doflow-run +23 -2
- package/core/shared/skills/do/SKILL.md +3 -3
- package/core/shared/skills/do-brainstorm/SKILL.md +26 -24
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +2 -1
- package/core/shared/skills/do-code-review/SKILL.md +7 -4
- package/core/shared/skills/do-design/SKILL.md +11 -5
- package/core/shared/skills/do-design/references/data_model_example.md +167 -0
- package/core/shared/skills/do-diagnose/SKILL.md +4 -2
- package/core/shared/skills/do-document/SKILL.md +1 -0
- package/core/shared/skills/do-document/references/embedded-research.md +1 -1
- package/core/shared/skills/do-execute-plan/SKILL.md +4 -2
- package/core/shared/skills/do-execute-plan/references/run_position.md +3 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +1 -1
- package/core/shared/skills/do-flow/SKILL.md +4 -4
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-implement/SKILL.md +9 -7
- package/core/shared/skills/do-plan/SKILL.md +9 -6
- package/core/shared/templates/doflow/data-model-template.md +136 -102
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/package.json +1 -1
- package/src/adapters/antigravity/index.js +54 -12
- package/src/adapters/claude/index.js +23 -2
- package/src/adapters/codex/agents.js +15 -1
- package/src/adapters/codex/config.js +5 -2
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +13 -0
- package/src/adapters/copilot/index.js +11 -4
- package/src/adapters/copy-tree.js +8 -2
- package/src/adapters/created-file.js +41 -0
- package/src/adapters/mcp-entries.js +11 -1
- package/src/adapters/opencode/index.js +32 -5
- package/src/adapters/pi/index.js +15 -4
- package/src/adapters/pi/mcp.js +44 -18
- package/src/cli/commands/remove.js +3 -1
- package/src/cli/commands/status.js +1 -1
- package/src/cli/commands/update.js +7 -3
- package/src/cli/index.js +43 -19
- package/src/cli/runtime-commands.js +13 -5
- package/src/cli/shared.js +5 -3
- package/src/helper/marker-merge.js +11 -7
- package/src/install/backup-set.js +7 -3
- package/src/install/backup.js +13 -7
- package/src/install/context.js +2 -1
- package/src/install/manifest.js +35 -11
- package/src/install/paths.js +38 -0
- package/src/install/targets.js +11 -2
- package/src/lifecycle/coverage.js +1 -30
- package/src/lifecycle/hook-tier.js +2 -2
- package/src/lifecycle/index.js +80 -3
- package/src/lifecycle/retired-harnesses.js +16 -1
- package/src/lifecycle/view.js +3 -2
- package/src/registry/index.js +21 -4
- package/src/release/docs-blocks.js +37 -53
- package/src/runtime/advice/doctor.js +2 -2
- package/src/runtime/brief-tools.js +2 -1
- package/src/runtime/capability-router.js +26 -3
- package/src/runtime/context/pointer.js +84 -17
- package/src/runtime/context-pack.js +52 -9
- package/src/runtime/dispatch/integrate.js +27 -8
- package/src/runtime/dispatch/loop.js +7 -0
- package/src/runtime/dispatch/verify-run.js +9 -3
- package/src/runtime/evidence-ledger.js +18 -0
- package/src/runtime/feature-pointer.js +10 -1
- package/src/runtime/health.js +68 -5
- package/src/runtime/implementation-gate.js +14 -6
- package/src/runtime/intake/engine.js +679 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/knowledge/code-retrieval.js +11 -5
- package/src/runtime/knowledge/semble-provider.js +12 -2
- package/src/runtime/lifecycle/cli.js +76 -6
- package/src/runtime/lifecycle/followup.js +32 -6
- package/src/runtime/lifecycle/overview.js +34 -3
- package/src/runtime/lifecycle/release.js +16 -3
- package/src/runtime/lifecycle/retention.js +12 -15
- package/src/runtime/lifecycle/store-upkeep.js +13 -3
- package/src/runtime/mask.js +3 -3
- package/src/runtime/mcp-consultations.js +11 -5
- package/src/runtime/packages/points.js +10 -0
- package/src/runtime/research-request.js +3 -1
- package/src/runtime/review/command.js +3 -7
- package/src/runtime/review/start.js +24 -10
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/gates.js +6 -3
- package/src/runtime/roadmap/loop.js +127 -33
- package/src/runtime/roadmap/merge.js +17 -8
- package/src/runtime/roadmap/rows.js +15 -5
- package/src/runtime/roadmap/seed.js +153 -41
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/settings/files.js +3 -1
- package/src/runtime/settings/prepared-run.js +12 -2
- package/src/runtime/settings/registry.js +1 -1
- package/src/runtime/settings/resolve.js +13 -1
- package/src/runtime/settings/write.js +73 -2
- package/src/runtime/slots.js +1 -1
- package/src/runtime/storage/registry.js +2 -2
- package/src/runtime/storage/roots.js +4 -18
- package/src/runtime/storage/rules.js +22 -46
- package/src/runtime/storage/work-status.js +14 -4
- package/src/runtime/task-classifier.js +39 -4
- package/src/runtime/task-scope.js +17 -2
- package/src/runtime/task-state.js +13 -7
- package/src/runtime/trace/analyze.js +4 -2
- package/src/runtime/trace/indicators.js +25 -20
- package/src/runtime/trace/spans.js +4 -5
- package/src/runtime/verb-view.js +268 -0
- package/src/runtime/verification/engine.js +0 -45
- package/src/runtime/worker/command.js +4 -2
- package/src/runtime/worker/launch.js +5 -37
- package/src/runtime/worker/readiness.js +10 -6
- package/src/runtime/worker/supervisor.js +18 -6
- package/src/runtime/workflow-orchestrator.js +39 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +0 -9
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -25
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -105
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -579
|
@@ -15,14 +15,40 @@ const followup = require('./followup.js');
|
|
|
15
15
|
const { StoreUnsafeError, StoreLockedError } = require('./event-store.js');
|
|
16
16
|
const { prepareStore } = require('./store-upkeep.js');
|
|
17
17
|
const { printSafe } = require('../mask.js');
|
|
18
|
-
const { buildOverview, initFeature, featureStatus, activeFeature } = require('./overview.js');
|
|
18
|
+
const { buildOverview, capOverviewBrief, initFeature, featureStatus, activeFeature } = require('./overview.js');
|
|
19
19
|
const { recordFeature, clearSlug } = require('../feature-pointer.js');
|
|
20
20
|
const { releaseFeatures, recordMerged } = require('./release.js');
|
|
21
21
|
const goal = require('./goal.js');
|
|
22
|
+
const { ViewUsageError, resolveView, projectBrief, viewLines, withEnvelope, serializeJson } = require('../verb-view.js');
|
|
22
23
|
|
|
23
24
|
const FOLLOWUP_ACTIONS = ['add', 'list', 'take', 'settle', 'promote', 'report'];
|
|
24
25
|
const LIFECYCLE_ACTIONS = ['overview', 'init', 'release', 'status', 'merged'];
|
|
25
26
|
const GOAL_ACTIONS = ['add', 'item', 'check', 'link', 'list', 'done'];
|
|
27
|
+
/** The brief `followup --action list` answer (feature 137, IC-004): the fields that let a caller choose an item. */
|
|
28
|
+
const FOLLOWUP_LIST_BRIEF = Object.freeze({
|
|
29
|
+
verb: 'followup',
|
|
30
|
+
action: 'list',
|
|
31
|
+
carry: Object.freeze(['state', 'ids', 'limit']),
|
|
32
|
+
keep: Object.freeze([
|
|
33
|
+
'$.ok', '$.action', '$.state', '$.count', '$.items[].id', '$.items[].state', '$.items[].statement',
|
|
34
|
+
'$.conflicts', '$.unreadable', '$.unreadableReasons',
|
|
35
|
+
]),
|
|
36
|
+
});
|
|
37
|
+
/** The brief `lifecycle --action overview` answer (feature 137, IC-006): keep paths, so a new full field is dropped until named here. */
|
|
38
|
+
const OVERVIEW_BRIEF = Object.freeze({
|
|
39
|
+
verb: 'lifecycle',
|
|
40
|
+
action: 'overview',
|
|
41
|
+
carry: Object.freeze(['maintain', 'since']),
|
|
42
|
+
keep: Object.freeze([
|
|
43
|
+
'$.ok', '$.mode', '$.releaseMode', '$.integrationRef', '$.gitError', '$.followups.open', '$.followups.shown',
|
|
44
|
+
'$.followups.items[].id', '$.followups.items[].statement', '$.followups.items[].source', '$.followups.items[].pending',
|
|
45
|
+
'$.intents', '$.goals[].goal', '$.goals[].outcome', '$.goals[].items', '$.goals[].proposeDone', '$.goals[].nudges',
|
|
46
|
+
'$.goals[].conflicts', '$.conflicts', '$.unreadable', '$.unreadableReasons', '$.failures', '$.reason', '$.note',
|
|
47
|
+
'$.integrationBehind', '$.pending', '$.omitted', '$.next',
|
|
48
|
+
]),
|
|
49
|
+
});
|
|
50
|
+
/** A brief list answers one page of this many items unless `--limit` says otherwise. */
|
|
51
|
+
const BRIEF_PAGE = 10;
|
|
26
52
|
/** The flags each goal action takes, besides --goal, --channel and --json; any other one given is a usage error. */
|
|
27
53
|
const GOAL_FLAGS = {
|
|
28
54
|
add: ['statement', 'item'], item: ['text'], check: ['item', 'evidence', 'unmet'],
|
|
@@ -40,6 +66,7 @@ function followupLines(result ) {
|
|
|
40
66
|
case 'list':
|
|
41
67
|
if (result.items.length === 0) lines.push(`No ${result.state === 'all' ? '' : `${result.state} `}follow-ups.`);
|
|
42
68
|
for (const i of result.items) lines.push(`${i.id} ${i.state}${i.takenBy ? ` by ${i.takenBy}` : ''}${i.intent ? ` -> ${i.intent}` : ''} (${sourceText(i.source)}, ${i.added}) ${i.statement}`);
|
|
69
|
+
for (const line of result.next) lines.push(`next: ${line}`);
|
|
43
70
|
break;
|
|
44
71
|
case 'take': lines.push(`${result.feature} took ${result.ids.join(', ')}`); break;
|
|
45
72
|
case 'settle': lines.push(`settled ${result.ids.join(', ')} as ${result.as}`); break;
|
|
@@ -129,7 +156,7 @@ function goalLines(r ) {
|
|
|
129
156
|
|
|
130
157
|
/** Prints a result and sets the exit status; shared by the verbs. */
|
|
131
158
|
function emit(result , json , lines ) {
|
|
132
|
-
if (json) console.log(
|
|
159
|
+
if (json) console.log(serializeJson(result));
|
|
133
160
|
else if (result.ok === false) console.log(printSafe(`${result.finding}: ${result.message}`));
|
|
134
161
|
else for (const line of lines(result)) console.log(printSafe(line));
|
|
135
162
|
return finishRuntime(result.ok === false ? 1 : 0);
|
|
@@ -140,7 +167,7 @@ function guarded(verb , action , json , fn ) {
|
|
|
140
167
|
try {
|
|
141
168
|
return fn();
|
|
142
169
|
} catch (error ) {
|
|
143
|
-
if (error instanceof followup.FollowupUsageError || error instanceof StoreUnsafeError) return usageError(verb, error.message, json);
|
|
170
|
+
if (error instanceof followup.FollowupUsageError || error instanceof ViewUsageError || error instanceof StoreUnsafeError) return usageError(verb, error.message, json);
|
|
144
171
|
if (error instanceof StoreLockedError) return emit({ ok: false, action, finding: 'store-locked', message: error.message }, json, () => []);
|
|
145
172
|
throw error;
|
|
146
173
|
}
|
|
@@ -157,6 +184,29 @@ function refuseGlobal(verb , global , json ) {
|
|
|
157
184
|
return global ? usageError(verb, `${verb} works on the project store at the repository root and refuses -g`, json) : null;
|
|
158
185
|
}
|
|
159
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Prints a `followup --action list` answer in the view it asked for (IC-004). Brief projects the page onto the keep
|
|
189
|
+
* list; a full call with no cursor and no limit is the service object, untouched. Every other answer carries the
|
|
190
|
+
* envelope and the next lines. Text mode shows the page's items in the usual format and the next lines only when
|
|
191
|
+
* the page is not the whole list.
|
|
192
|
+
*/
|
|
193
|
+
function emitList(result , view , flags , json ) {
|
|
194
|
+
if (result.ok === false) return emit(result, json, followupLines);
|
|
195
|
+
const paged = result.shown !== undefined;
|
|
196
|
+
const carried = { state: flags.state, ids: flags.ids, limit: flags.limit };
|
|
197
|
+
const lines = !paged ? [] : [
|
|
198
|
+
...viewLines('page', { spec: FOLLOWUP_LIST_BRIEF, carried, nextCursor: result.nextCursor, view, json: Boolean(json) }),
|
|
199
|
+
...(view === 'brief' ? viewLines('page-full', { spec: FOLLOWUP_LIST_BRIEF, carried, cursor: flags.cursor, shown: result.shown, json: Boolean(json) }) : []),
|
|
200
|
+
];
|
|
201
|
+
if (!json) return emit({ ...result, next: result.shown < result.count ? lines : [] }, false, followupLines);
|
|
202
|
+
if (view === 'brief') {
|
|
203
|
+
const body = projectBrief(result, FOLLOWUP_LIST_BRIEF.keep);
|
|
204
|
+
return emit(withEnvelope(body, { view, shown: result.shown, nextCursor: result.nextCursor, next: lines }), true, followupLines);
|
|
205
|
+
}
|
|
206
|
+
if (!paged) return emit(result, true, followupLines);
|
|
207
|
+
return emit(withEnvelope(result, { view, shown: result.shown, nextCursor: result.nextCursor, next: lines }), true, followupLines);
|
|
208
|
+
}
|
|
209
|
+
|
|
160
210
|
/**
|
|
161
211
|
* `followup` verb. Actions: add, list, take, settle, promote, report.
|
|
162
212
|
* @param {Object} options
|
|
@@ -175,6 +225,12 @@ function handleFollowupCommand({ action, cwd, global = false, slug = null, json
|
|
|
175
225
|
if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to --action report only`);
|
|
176
226
|
}
|
|
177
227
|
}
|
|
228
|
+
if (action !== 'list') {
|
|
229
|
+
for (const name of ['view', 'cursor', 'limit']) {
|
|
230
|
+
if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to --action list only`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const view = action === 'list' ? resolveView(flags.view, 'brief') : null;
|
|
178
234
|
for (const name of ['tag', 'confirm', 'exclude']) {
|
|
179
235
|
if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to the lifecycle verb's --action release only, not to followup`);
|
|
180
236
|
}
|
|
@@ -198,7 +254,9 @@ function handleFollowupCommand({ action, cwd, global = false, slug = null, json
|
|
|
198
254
|
defaults: { stage: flags.stage, slug, source: flags.source, taskClass: flags.taskClass, taskId: flags.taskId, release: flags.release, batch },
|
|
199
255
|
});
|
|
200
256
|
} else if (action === 'list') {
|
|
201
|
-
|
|
257
|
+
return emitList(followup.listFollowups({
|
|
258
|
+
root, state: flags.state, ids: flags.ids, cursor: flags.cursor, limit: flags.limit, defaultLimit: view === 'brief' ? BRIEF_PAGE : null,
|
|
259
|
+
}), view, flags, json);
|
|
202
260
|
} else if (action === 'take') {
|
|
203
261
|
result = followup.takeFollowups({ root, ids: flags.ids, slug, channel: flags.channel });
|
|
204
262
|
} else if (action === 'settle') {
|
|
@@ -230,13 +288,25 @@ function handleLifecycleCommand({ action, cwd, global = false, slug = null, json
|
|
|
230
288
|
if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to --action release only`);
|
|
231
289
|
}
|
|
232
290
|
}
|
|
291
|
+
if (act !== 'overview' && flags.view !== undefined && flags.view !== false) throw new followup.FollowupUsageError('--view applies to --action overview only');
|
|
292
|
+
const view = act === 'overview' ? resolveView(flags.view, 'full') : null;
|
|
233
293
|
if (flags.reason !== undefined && act !== 'merged') throw new followup.FollowupUsageError('--reason applies to --action merged only');
|
|
234
294
|
const root = projectRoot(cwd || process.cwd());
|
|
235
295
|
const refusedStore = upkeep('lifecycle', act, root);
|
|
236
296
|
if (refusedStore) return emit(refusedStore, json, lifecycleLines);
|
|
237
297
|
let result;
|
|
238
|
-
if (act === 'overview')
|
|
239
|
-
|
|
298
|
+
if (act === 'overview') {
|
|
299
|
+
result = buildOverview({ root, maintain: Boolean(flags.maintain), since: flags.since });
|
|
300
|
+
// The text answer is the same in both views (IC-001); only the JSON brief is cut to one page of items. Maintain
|
|
301
|
+
// keeps its 50: that loop settles every pending item. The next lines name the paged list and carry no full-view line.
|
|
302
|
+
if (view === 'brief' && json) {
|
|
303
|
+
const items = flags.maintain ? result.followups.items : result.followups.items.slice(0, BRIEF_PAGE);
|
|
304
|
+
const brief = capOverviewBrief({ ...result, followups: { ...result.followups, shown: items.length, items } });
|
|
305
|
+
// Something cut from a list: the next lines also say how to get everything.
|
|
306
|
+
const next = brief.omitted ? [...result.next, ...viewLines('full', { spec: OVERVIEW_BRIEF, carried: { maintain: flags.maintain, since: flags.since }, json: true })] : undefined;
|
|
307
|
+
return emit(withEnvelope(projectBrief(brief, OVERVIEW_BRIEF.keep), { view, ...(next ? { next } : {}) }), true, lifecycleLines);
|
|
308
|
+
}
|
|
309
|
+
} else if (act === 'init') {
|
|
240
310
|
result = initFeature({ root, slug, take: flags.take, intent: flags.intent, goal: flags.goal });
|
|
241
311
|
if (result.ok) recordFeature({ storeRoot: root, cwd: cwd || process.cwd(), slug });
|
|
242
312
|
}
|
|
@@ -22,6 +22,7 @@ const { STAGES: CHAIN_STAGES } = require('../decision-register.js');
|
|
|
22
22
|
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
|
|
23
23
|
const { resolveActiveFeature } = require('../feature-resolve.js');
|
|
24
24
|
const { SETTLE_AS, withDerivedDone } = require('./fold.js');
|
|
25
|
+
const { pageItems } = require('../verb-view.js');
|
|
25
26
|
const { appendEvents, readFold, byFromChannel, randomChars } = require('./event-store.js');
|
|
26
27
|
const { deriveStatuses } = require('./status.js');
|
|
27
28
|
const { writeIntent, kebabTitle } = require('./intent-writer.js');
|
|
@@ -334,12 +335,37 @@ function reportFollowup({ root, statement, input, release, feature, channel, now
|
|
|
334
335
|
|
|
335
336
|
// ── list ───────────────────────────────────────────────────────────────────────────────────────
|
|
336
337
|
|
|
337
|
-
/**
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
/** The most items one list answer holds, however large a `--limit` asks for (IC-004). */
|
|
339
|
+
const LIST_MAX_LIMIT = 100;
|
|
340
|
+
|
|
341
|
+
/** The key the fold orders on and a list cursor names: the creating event's time in ms, then its event id (IC-008). */
|
|
342
|
+
function followupOrderKey(item ) {
|
|
343
|
+
const created = item.history[0];
|
|
344
|
+
return { atMs: Date.parse(created.at), event: created.event };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* IC-006 `list`: `state` is open (default), taken, done, dismissed or all. `ids` keeps the named items (and, with no
|
|
349
|
+
* `state`, looks in every state); an id that is no item at all is the `unknown-id` refusal. With `cursor`, `limit` or
|
|
350
|
+
* `defaultLimit` the answer holds one page of `items` plus `shown` and `nextCursor`; with none it is the whole list.
|
|
351
|
+
*/
|
|
352
|
+
function listFollowups({ root, state, ids, cursor, limit, defaultLimit = null, hasBody, fsImpl = nodeFs, statuses, now = new Date() } ) {
|
|
353
|
+
const wanted = ids === undefined ? null : parseIds(ids);
|
|
354
|
+
const wantedState = state === undefined ? (wanted ? 'all' : 'open') : state;
|
|
355
|
+
if (wantedState !== 'all' && !STATES.includes(wantedState)) throw new FollowupUsageError(`--state must be one of ${[...STATES, 'all'].join(', ')} (got '${wantedState}')`);
|
|
340
356
|
const { fold, followups } = loadFollowups(root, { hasBody, fsImpl, statuses, now });
|
|
341
|
-
|
|
342
|
-
|
|
357
|
+
let matched = followups;
|
|
358
|
+
if (wanted) {
|
|
359
|
+
const known = new Set(followups.map((item ) => item.id));
|
|
360
|
+
const unknown = wanted.filter((id) => !known.has(id));
|
|
361
|
+
if (unknown.length) return refusal('list', 'unknown-id', `${unknown.join(', ')} ${unknown.length === 1 ? 'is' : 'are'} not a follow-up in this store. Nothing was listed.`);
|
|
362
|
+
matched = followups.filter((item ) => wanted.includes(item.id));
|
|
363
|
+
}
|
|
364
|
+
if (wantedState !== 'all') matched = matched.filter((item ) => item.state === wantedState);
|
|
365
|
+
const answer = { ok: true, action: 'list', state: wantedState, count: matched.length, items: matched, conflicts: fold.conflicts, unreadable: fold.unreadable, unreadableReasons: fold.unreadableReasons, next: [] };
|
|
366
|
+
if (cursor === undefined && limit === undefined && defaultLimit === null) return answer;
|
|
367
|
+
const page = pageItems(matched, { cursor, limit, defaultLimit, maxLimit: LIST_MAX_LIMIT, key: followupOrderKey });
|
|
368
|
+
return { ...answer, items: page.items, shown: page.shown, nextCursor: page.nextCursor };
|
|
343
369
|
}
|
|
344
370
|
|
|
345
371
|
// ── take, settle, promote ──────────────────────────────────────────────────────────────────────
|
|
@@ -431,7 +457,7 @@ function promoteFollowups({ root, ids, title, channel, now = new Date(), fsImpl
|
|
|
431
457
|
}
|
|
432
458
|
|
|
433
459
|
module.exports = {
|
|
434
|
-
addFollowups, reportFollowup, listFollowups, takeFollowups, settleFollowups, promoteFollowups,
|
|
460
|
+
addFollowups, reportFollowup, listFollowups, followupOrderKey, takeFollowups, settleFollowups, promoteFollowups,
|
|
435
461
|
loadFollowups, readBatchFile, resolveFeatureSlug, parseIds, oneLine, channelBy,
|
|
436
462
|
FollowupUsageError, STAGES, STATES, SOURCE_KINDS, STATEMENT_MAX,
|
|
437
463
|
};
|
|
@@ -17,6 +17,7 @@ const { appendEvents, readFold, readEvents } = require('./event-store.js');
|
|
|
17
17
|
const { deriveStatuses, bucketize, behindNote } = require('./status.js');
|
|
18
18
|
const { FollowupUsageError, parseIds } = require('./followup.js');
|
|
19
19
|
const { describeActive } = require('../feature-pointer.js');
|
|
20
|
+
const { commandLine } = require('../verb-view.js');
|
|
20
21
|
|
|
21
22
|
/** The failure store, or null when its module cannot be loaded: the overview then reports no failures. */
|
|
22
23
|
function failureStoreOrNull() {
|
|
@@ -27,6 +28,8 @@ const DISCOVERY_SHOWN = 15;
|
|
|
27
28
|
const MAINTAIN_SHOWN = 50;
|
|
28
29
|
/** Slugs shown per finished or awaiting-release bucket: those lists only grow, and the overview runs on every brainstorm. */
|
|
29
30
|
const FEATURES_SHOWN = 10;
|
|
31
|
+
/** Entries per list in the JSON brief overview: intents, the ids of one intent, the nudges of one goal (137 FU-2jtkch). */
|
|
32
|
+
const BRIEF_LIST_SHOWN = 10;
|
|
30
33
|
|
|
31
34
|
const refusal = (action , finding , message ) => ({ ok: false, action, finding, message });
|
|
32
35
|
|
|
@@ -115,7 +118,10 @@ function goalView(goal , features , statuses , conflicts = [])
|
|
|
115
118
|
const met = goal.items.filter((i ) => i.met).length;
|
|
116
119
|
const nudges = [];
|
|
117
120
|
if (buckets.finished.length) {
|
|
118
|
-
|
|
121
|
+
// The nudge names the most recent finished features only; `features.finished` still lists every one.
|
|
122
|
+
const finished = shownBucket(buckets.finished);
|
|
123
|
+
const who = buckets.finished.length === 1 ? `linked feature ${buckets.finished[0]} is finished`
|
|
124
|
+
: `linked features ${finished.shown.join(', ')}${finished.more ? ` (+${finished.more} more)` : ''} are finished`;
|
|
119
125
|
for (const item of goal.items.filter((i ) => !i.met)) nudges.push(`${item.id} is unchecked and ${who}`);
|
|
120
126
|
}
|
|
121
127
|
return {
|
|
@@ -130,7 +136,7 @@ function goalView(goal , features , statuses , conflicts = [])
|
|
|
130
136
|
|
|
131
137
|
function nextLines({ shown, open, items, intents, goals, maintain, pendingItems, failures } ) {
|
|
132
138
|
const next = [];
|
|
133
|
-
if (shown < open) next.push(
|
|
139
|
+
if (shown < open) next.push(`Page through all ${open} open follow-ups: ${commandLine('followup', 'list', {})}`);
|
|
134
140
|
if (items.length) next.push(`Take items into the new feature when its folder exists: doflow-run lifecycle --action init --slug <slug> --take ${items[0].id}`);
|
|
135
141
|
if (intents.length) {
|
|
136
142
|
next.push(`Start from a promoted intent: /do-brainstorm --intent ${intents[0].path}, then doflow-run lifecycle --action init --slug <slug> --intent ${intents[0].path}`);
|
|
@@ -159,6 +165,31 @@ function nextLines({ shown, open, items, intents, goals, maintain, pendingItems,
|
|
|
159
165
|
return next;
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The JSON brief form of an overview answer: at most BRIEF_LIST_SHOWN intents, ids per intent and nudges per goal. What
|
|
170
|
+
* was cut is counted in `omitted` (absent when nothing was cut): `intents`, and `intentItems`, `nudges` and
|
|
171
|
+
* `finished` (the finished features a goal's nudges leave out of their text) per intent or goal that exceeds the
|
|
172
|
+
* bound. The full view lists everything.
|
|
173
|
+
*/
|
|
174
|
+
function capOverviewBrief(result ) {
|
|
175
|
+
const omitted = {};
|
|
176
|
+
const intents = result.intents.slice(0, BRIEF_LIST_SHOWN).map((intent ) => {
|
|
177
|
+
if (intent.items.length > BRIEF_LIST_SHOWN) {
|
|
178
|
+
(omitted.intentItems ??= []).push({ path: intent.path, total: intent.items.length, more: intent.items.length - BRIEF_LIST_SHOWN });
|
|
179
|
+
}
|
|
180
|
+
return { ...intent, items: intent.items.slice(0, BRIEF_LIST_SHOWN) };
|
|
181
|
+
});
|
|
182
|
+
if (result.intents.length > BRIEF_LIST_SHOWN) omitted.intents = { total: result.intents.length, more: result.intents.length - BRIEF_LIST_SHOWN };
|
|
183
|
+
const goals = result.goals.map((goal ) => {
|
|
184
|
+
const finished = goal.features.finished.length;
|
|
185
|
+
if (finished > FEATURES_SHOWN) (omitted.finished ??= []).push({ goal: goal.goal, total: finished, more: finished - FEATURES_SHOWN });
|
|
186
|
+
if (goal.nudges.length <= BRIEF_LIST_SHOWN) return goal;
|
|
187
|
+
(omitted.nudges ??= []).push({ goal: goal.goal, total: goal.nudges.length, more: goal.nudges.length - BRIEF_LIST_SHOWN });
|
|
188
|
+
return { ...goal, nudges: goal.nudges.slice(0, BRIEF_LIST_SHOWN) };
|
|
189
|
+
});
|
|
190
|
+
return { ...result, intents, goals, ...(Object.keys(omitted).length ? { omitted } : {}) };
|
|
191
|
+
}
|
|
192
|
+
|
|
162
193
|
/**
|
|
163
194
|
* IC-007.
|
|
164
195
|
* @param {Object} options
|
|
@@ -312,4 +343,4 @@ function activeFeature({ root, cwd, now = new Date(), fsImpl = nodeFs } ) {
|
|
|
312
343
|
return derived.ok ? { ...derived, active } : { ...result, slug: active.slug, message: `${active.slug} is tracked by its run only; \`lifecycle --action init\` to track it.` };
|
|
313
344
|
}
|
|
314
345
|
|
|
315
|
-
module.exports = { buildOverview, initFeature, featureStatus, activeFeature, goalView, goalConflicts, goalConflictNext, DISCOVERY_SHOWN, MAINTAIN_SHOWN, FEATURES_SHOWN };
|
|
346
|
+
module.exports = { buildOverview, capOverviewBrief, initFeature, featureStatus, activeFeature, goalView, goalConflicts, goalConflictNext, DISCOVERY_SHOWN, MAINTAIN_SHOWN, FEATURES_SHOWN };
|
|
@@ -39,6 +39,7 @@ const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
|
|
|
39
39
|
const { appendEvents, readFold } = require('./event-store.js');
|
|
40
40
|
const { deriveStatuses, readGitFacts, behindNote, subjectNames, readMerges, readBranchTips, readHistory } = require('./status.js');
|
|
41
41
|
const { FollowupUsageError, oneLine, channelBy } = require('./followup.js');
|
|
42
|
+
const { parseSemVer, compareSemVer } = require('../../helper/semver.js');
|
|
42
43
|
|
|
43
44
|
/** The release-tag pattern of IC-020, the one `git-state` filters by. */
|
|
44
45
|
const RELEASE_TAG = /^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/;
|
|
@@ -125,13 +126,25 @@ function releaseFeatures({ root, tag, confirm = false, features, exclude, channe
|
|
|
125
126
|
return refusal('release', 'untracked-feature', `${untracked.join(', ')} ${untracked.length === 1 ? 'is' : 'are'} not tracked, so ${untracked.length === 1 ? 'it' : 'they'} cannot be added to or excluded from a release; run doflow-run lifecycle --action init --slug <slug> first. Nothing was written.`);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
// DEC-030: containment in the tag being recorded never finishes a feature before its record exists.
|
|
129
|
-
|
|
129
|
+
// DEC-030: containment in the tag being recorded never finishes a feature before its record exists. Of the
|
|
130
|
+
// other tags only an unrecorded one that is stable and earlier than `tag` (lower version, or a strict ancestor of it)
|
|
131
|
+
// may take a feature out of this release: a pre-release (v1.0.0-rc.1 before v1.0.0) or a later tag
|
|
132
|
+
// (v2.0.0 when recording v1.0.0) does not. A tag with a record is left in: status reads it by its record.
|
|
133
|
+
const tagCommit = commitOf(root, `refs/tags/${tag}`);
|
|
134
|
+
const recordedTags = new Set(fold.releases.map((r ) => r.tag));
|
|
135
|
+
const shadows = (name ) => {
|
|
136
|
+
const parsed = parseSemVer(name.slice(1));
|
|
137
|
+
if (!parsed || parsed.prerelease.length > 0) return false;
|
|
138
|
+
if (compareSemVer(name.slice(1), tag.slice(1)) < 0) return true;
|
|
139
|
+
// A strict ancestor only: a higher-versioned tag on the very same commit is not earlier.
|
|
140
|
+
const commit = tagCommit && commitOf(root, `refs/tags/${name}`);
|
|
141
|
+
return Boolean(commit) && commit !== tagCommit && isAncestor(root, commit, tagCommit);
|
|
142
|
+
};
|
|
143
|
+
const otherTags = { ...facts, release_tags: facts.release_tags.filter((name ) => name !== tag && (recordedTags.has(name) || shadows(name))) };
|
|
130
144
|
const derived = deriveStatuses({ root, fold, facts: otherTags });
|
|
131
145
|
if (derived.releaseMode === 'unknown' || !derived.integrationSha) return refusal('release', 'no-integration-ref', `${derived.reason || `${facts.integration_ref} does not resolve to a commit`}. Nothing was written.`);
|
|
132
146
|
|
|
133
147
|
// X: the tag's commit once it exists, else the commit the status view pinned the integration ref to (DEC-044).
|
|
134
|
-
const tagCommit = commitOf(root, `refs/tags/${tag}`);
|
|
135
148
|
const bound = tagCommit || derived.integrationSha;
|
|
136
149
|
const previousTag = previousTagOf(root, { tag, facts, bound });
|
|
137
150
|
// A hotfix merged only into the tag's own history has no evidence on the integration chain: derive against the tag as well.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const nodeFs = require('node:fs');
|
|
20
20
|
const { isDeepStrictEqual } = require('node:util');
|
|
21
|
-
const { loadSettings } = require('../settings/resolve.js');
|
|
21
|
+
const { loadSettings, registryOn } = require('../settings/resolve.js');
|
|
22
22
|
const { foldInto, finalize, EVENT_TYPES } = require('./fold.js');
|
|
23
23
|
|
|
24
24
|
const HOUR_MS = 3600000;
|
|
@@ -53,14 +53,7 @@ function retentionWindow({ env = process.env, projectRoot, fsImpl = nodeFs }
|
|
|
53
53
|
const parsed = parseWindow(env);
|
|
54
54
|
if (parsed.state === 'invalid') return parsed;
|
|
55
55
|
try {
|
|
56
|
-
|
|
57
|
-
if (fsImpl !== nodeFs) {
|
|
58
|
-
const { loadSettingsRegistry } = require('../settings/registry.js');
|
|
59
|
-
const loaded = loadSettingsRegistry({ fsImpl });
|
|
60
|
-
if (!loaded.ok) throw new Error(loaded.detail);
|
|
61
|
-
settingsRegistry = loaded.registry;
|
|
62
|
-
}
|
|
63
|
-
const view = loadSettings({ projectRoot, env, fsImpl, ...(settingsRegistry ? { registry: settingsRegistry } : {}) });
|
|
56
|
+
const view = loadSettings({ projectRoot, env, fsImpl, ...registryOn(fsImpl) });
|
|
64
57
|
const retention = view.get('storage.retentionHours', 'project');
|
|
65
58
|
if (retention.state === 'invalid') return { state: 'invalid', raw: invalidRaw(retention, env) };
|
|
66
59
|
if (retention.explicit) {
|
|
@@ -136,10 +129,12 @@ function unitsOf(events , shouldStop ) {
|
|
|
136
129
|
}
|
|
137
130
|
|
|
138
131
|
/**
|
|
139
|
-
* Every string value and every object key
|
|
140
|
-
* does not know (a newer DoFlow wrote them). The fold ignores them, so retention
|
|
141
|
-
*
|
|
142
|
-
*
|
|
132
|
+
* Every string value in the envelope, and every object key inside it, of the events whose type this
|
|
133
|
+
* version's fold does not know (a newer DoFlow wrote them). The fold ignores them, so retention
|
|
134
|
+
* cannot tell what they change; a unit that one of them names, by item id, event id or event file
|
|
135
|
+
* name, is kept whole rather than deleted from under it. The envelope's own field names are its
|
|
136
|
+
* shape, not references. Exact matches only: keeping too little is a lost record, keeping too much
|
|
137
|
+
* only a file that stays.
|
|
143
138
|
* @returns {Set<string>|null} null when `shouldStop` fired
|
|
144
139
|
*/
|
|
145
140
|
function unknownTypeStrings(events , shouldStop ) {
|
|
@@ -148,7 +143,7 @@ function unknownTypeStrings(events , shouldStop )
|
|
|
148
143
|
for (const event of events) {
|
|
149
144
|
if (shouldStop?.()) return null;
|
|
150
145
|
if (known.has(event.type)) continue;
|
|
151
|
-
const pending =
|
|
146
|
+
const pending = Object.values(event);
|
|
152
147
|
while (pending.length > 0) {
|
|
153
148
|
if (shouldStop?.()) return null;
|
|
154
149
|
const value = pending.pop();
|
|
@@ -260,7 +255,9 @@ function selectExpired(events , { now, windowMs, shouldStop } = {})
|
|
|
260
255
|
};
|
|
261
256
|
const baseSelected = safeSubset(candidates);
|
|
262
257
|
if (!baseSelected) return incomplete();
|
|
263
|
-
const
|
|
258
|
+
const namedByUnknown = (unit ) => [...unit.items].some((key ) => mentioned.has(key.slice(key.indexOf(':') + 1)))
|
|
259
|
+
|| unit.events.some((event ) => mentioned.has(event.id) || mentioned.has(`${event.id}.json`));
|
|
260
|
+
const unprotected = baseSelected.filter((unit ) => !namedByUnknown(unit));
|
|
264
261
|
// Dropping units can leave a set the refold no longer accepts; check it again, inside the subset.
|
|
265
262
|
const selected = unprotected.length === baseSelected.length ? baseSelected : safeSubset(unprotected);
|
|
266
263
|
if (!selected) return incomplete();
|
|
@@ -342,9 +342,9 @@ function retainLocked(root , options = {}) {
|
|
|
342
342
|
if (!release) return outcome(0, [], true);
|
|
343
343
|
try {
|
|
344
344
|
const swept = sweepStaleTmp(fsImpl, root, now, staleTmpMs, { maxFiles, shouldStop });
|
|
345
|
-
if (swept.partial) return outcome(
|
|
345
|
+
if (swept.partial) return outcome(swept.removed, [], true);
|
|
346
346
|
const result = retain(fsImpl, root, window, now, { maxFiles: Math.max(0, maxFiles - swept.removed), shouldStop });
|
|
347
|
-
return outcome(result.removedCount, result.warning, result.partial, result.selected, result.code);
|
|
347
|
+
return outcome(swept.removed + result.removedCount, result.warning, result.partial, result.selected, result.code);
|
|
348
348
|
} finally {
|
|
349
349
|
release();
|
|
350
350
|
}
|
|
@@ -358,6 +358,16 @@ function needsPrune(fsImpl , root , window , now ) {
|
|
|
358
358
|
return selectExpired(readEvents(root, { fsImpl }).events, { now, windowMs: window.ms }).files.length > 0;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Whether the old `agent-docs/lifecycle` copy still waits to be copied. Read-only (no lock, no
|
|
363
|
+
* write, `DOFLOW_UPKEEP` not consulted), for `doctor`; an unreadable folder counts as not pending.
|
|
364
|
+
*/
|
|
365
|
+
function migrationPending(root , { fsImpl = nodeFs } = {}) {
|
|
366
|
+
try {
|
|
367
|
+
return !hasMarker(fsImpl, root) && !storeHasEvents(fsImpl, root) && legacyEventFiles(fsImpl, root).length > 0;
|
|
368
|
+
} catch { return false; }
|
|
369
|
+
}
|
|
370
|
+
|
|
361
371
|
/**
|
|
362
372
|
* Prepares the project store for one verb invocation.
|
|
363
373
|
* @param {string} root projectRoot() result
|
|
@@ -439,4 +449,4 @@ function prepareStore(root , { env = process.env, now = new Date(), fsImp
|
|
|
439
449
|
}
|
|
440
450
|
}
|
|
441
451
|
|
|
442
|
-
module.exports = { prepareStore, retainLocked, removedLine, STALE_TMP_MS, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };
|
|
452
|
+
module.exports = { prepareStore, migrationPending, retainLocked, removedLine, STALE_TMP_MS, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };
|
package/src/runtime/mask.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* Rules run in the table's order. Normalisation (rules 10 to 13) is a separate step applied to
|
|
13
13
|
* failure messages only, after masking.
|
|
14
14
|
*
|
|
15
|
-
* The module has no I/O and no state; the home directory is read from `HOME`
|
|
16
|
-
* passes one.
|
|
15
|
+
* The module has no I/O and no state; the home directory is read from `HOME` (`USERPROFILE` when
|
|
16
|
+
* `HOME` is absent or empty, as on Windows) unless the caller passes one.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
const MASKED = '<masked>';
|
|
@@ -152,7 +152,7 @@ function escapeRegExp(text ) { return text.replace(/[.*+?^${}()|[
|
|
|
152
152
|
|
|
153
153
|
/** The home directory to abbreviate: an absolute path other than the file system root. */
|
|
154
154
|
function homeOf(options ) {
|
|
155
|
-
const raw = options && 'home' in options ? options.home : process.env.HOME;
|
|
155
|
+
const raw = options && 'home' in options ? options.home : (process.env.HOME || process.env.USERPROFILE);
|
|
156
156
|
if (typeof raw !== 'string' || !(raw.startsWith('/') || /^[A-Za-z]:[\\/]/.test(raw))) return null;
|
|
157
157
|
const home = raw.replace(/[\\/]+$/, '');
|
|
158
158
|
return home.length > 1 ? home : null;
|
|
@@ -230,14 +230,15 @@ const bySortKey = (a , b ) => (a < b ? -1 : a > b ? 1 : 0);
|
|
|
230
230
|
* Injected readers (all optional; a missing one contributes nothing):
|
|
231
231
|
* - `ledger.queryEvidence({taskId})` -> evidence items (active ones count)
|
|
232
232
|
* - `requests.list(taskId)` -> research requests
|
|
233
|
-
* - `briefRecords` -> IC-018 records already parsed, each `{ runTaskId, stage, tasks, expected }`
|
|
233
|
+
* - `briefRecords` -> IC-018 records already parsed, each `{ runTaskId, stage, createdAt, tasks, expected }`
|
|
234
234
|
* - `readReport(path)` -> report text, or null when the file is missing
|
|
235
235
|
* - `reach.anyHarnessHas(server)` -> true | false | null (unknown); never decides an outcome
|
|
236
236
|
* - `reach.harnessWith(server)` -> the display name of a harness that has the server, or null
|
|
237
237
|
* - `harnessName(id)` -> a harness id's display name (for a brief record's `harness`)
|
|
238
|
-
* - `addCommand(server)` -> the text after "add it:" in the unavailable notice
|
|
238
|
+
* - `addCommand(server, harnessId)` -> the text after "add it:" in the unavailable notice; `harnessId` is the harness
|
|
239
|
+
* of the brief that found the server unreachable, or null when no brief names one
|
|
239
240
|
* - `since` -> ISO time of the previous stage's completion (or the run start); rule 3 counts only
|
|
240
|
-
* evidence observed after it
|
|
241
|
+
* evidence observed after it, and only brief records created after it
|
|
241
242
|
*
|
|
242
243
|
* @returns {{ record: object, warnings: object[], notices: object[], evidenceToAdd: object[] }}
|
|
243
244
|
*/
|
|
@@ -266,8 +267,11 @@ function resolveConsultations({
|
|
|
266
267
|
// ingested (design A6).
|
|
267
268
|
const toolsUsed = [];
|
|
268
269
|
const fromReports = new Map ();
|
|
270
|
+
// A stage visited twice leaves briefs from the earlier visit: when `since` is known only a brief created
|
|
271
|
+
// after it counts, and a record with no creation time (written before it was stamped) cannot be placed.
|
|
269
272
|
const records = (Array.isArray(briefRecords) ? briefRecords : [])
|
|
270
|
-
.filter((record ) => record && stageId && taskId && record.stage === stageId && record.runTaskId === taskId
|
|
273
|
+
.filter((record ) => record && stageId && taskId && record.stage === stageId && record.runTaskId === taskId
|
|
274
|
+
&& (since === null || (typeof record.createdAt === 'string' && record.createdAt > since)));
|
|
271
275
|
const reports = [];
|
|
272
276
|
for (const record of records) {
|
|
273
277
|
for (const task of record.tasks || []) {
|
|
@@ -347,10 +351,12 @@ function resolveConsultations({
|
|
|
347
351
|
// brief writes `unreachable` only where every config the harness merges was read (IC-017).
|
|
348
352
|
const unreachable = new Map ();
|
|
349
353
|
const reachableOn = new Map ();
|
|
354
|
+
const unreachableOn = new Map ();
|
|
350
355
|
for (const record of records) {
|
|
351
356
|
for (const expected of record.expected || []) {
|
|
352
357
|
if (!expected || typeof expected.server !== 'string') continue;
|
|
353
358
|
if (expected.reach === 'unreachable' && !unreachable.has(expected.server)) unreachable.set(expected.server, record.tasks?.[0]?.id);
|
|
359
|
+
if (expected.reach === 'unreachable' && !unreachableOn.has(expected.server)) unreachableOn.set(expected.server, record.harness ?? null);
|
|
354
360
|
if (expected.reach === 'reachable' && !reachableOn.has(expected.server)) reachableOn.set(expected.server, record.harness ?? null);
|
|
355
361
|
}
|
|
356
362
|
}
|
|
@@ -415,7 +421,7 @@ function resolveConsultations({
|
|
|
415
421
|
text: `[WARN] mcp: ${server} reported unavailable but ${has} has it configured`,
|
|
416
422
|
});
|
|
417
423
|
}
|
|
418
|
-
const add = !has && typeof addCommand === 'function' ? addCommand(server) : null;
|
|
424
|
+
const add = !has && typeof addCommand === 'function' ? addCommand(server, unreachableOn.get(server) ?? null) : null;
|
|
419
425
|
notices.push({
|
|
420
426
|
kind: 'unavailable', server,
|
|
421
427
|
text: `[INFO] mcp: ${server} unavailable to ${skill}${add ? `; add it: ${add}` : ''}`,
|
|
@@ -42,6 +42,15 @@ function presetValidator(kind ) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** An intake category contributed by a package (136 IC-006): IC-005's category shape, whose `id` is the contribution id. */
|
|
46
|
+
function intakeCategoryValidator(id , entry , context = {}) {
|
|
47
|
+
const { validateCategory } = require('../intake/taxonomy.js');
|
|
48
|
+
const location = context.packageId ? `package '${context.packageId}' intake category '${id}'` : `intake category '${id}'`;
|
|
49
|
+
const errors = validateCategory(entry, location);
|
|
50
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry) && entry.id !== id) errors.push(`${location}.id must equal the contribution id '${id}'`);
|
|
51
|
+
return errors;
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
function workerValidator(id , worker , context = {}) {
|
|
46
55
|
const errors = [];
|
|
47
56
|
const location = context.packageId ? `package '${context.packageId}' worker '${id}'` : `worker '${id}'`;
|
|
@@ -168,6 +177,7 @@ const POINT_VALIDATORS
|
|
|
168
177
|
'agent-v1': agentValidator,
|
|
169
178
|
'worker-v1': workerValidator,
|
|
170
179
|
'capability-provider-v1': capabilityProviderValidator,
|
|
180
|
+
'intake-category-v1': intakeCategoryValidator,
|
|
171
181
|
});
|
|
172
182
|
|
|
173
183
|
function isObject(value ) {
|
|
@@ -95,7 +95,9 @@ class ResearchRequestStore {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
stageFor(taskId , stageId ) {
|
|
98
|
-
|
|
98
|
+
// The run is read the way every run read is: no link followed, no FIFO or device opened (FU-d88kp0).
|
|
99
|
+
const { readRunRecord } = require('./implementation-gate.js');
|
|
100
|
+
const run = readRunRecord(this.fsImpl, this.runFile(taskId), path.join(this.projectRoot, '.doflow', 'state', 'orchestration'));
|
|
99
101
|
if (!run || run.taskClass !== 'feature' || run.state !== 'RUNNING') {
|
|
100
102
|
throw new Error(`Task '${taskId}' has no active feature stage`);
|
|
101
103
|
}
|
|
@@ -25,15 +25,10 @@ const TELEMETRY_COUNT_KEYS = Object.freeze([
|
|
|
25
25
|
'plausible', 'unverified_blockers', 'bug_classes',
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
-
// Inherited location variables would point the check at another repository (C-8).
|
|
29
|
-
const GIT_LOCATION_KEYS = Object.freeze([
|
|
30
|
-
'GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_COMMON_DIR',
|
|
31
|
-
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
28
|
function defaultGit(args , cwd , env ) {
|
|
35
29
|
const gitEnv = { ...env };
|
|
36
|
-
|
|
30
|
+
// Inherited location variables would point the check at another repository (C-8); the list is verification's.
|
|
31
|
+
for (const key of require('../dispatch/verify-run.js').REPO_ENV) delete gitEnv[key];
|
|
37
32
|
return spawnSync('git', ['-C', cwd, ...args], { cwd, env: gitEnv, stdio: 'ignore' });
|
|
38
33
|
}
|
|
39
34
|
|
|
@@ -112,6 +107,7 @@ function handleReviewCommand({
|
|
|
112
107
|
}
|
|
113
108
|
return handleReviewStart({ slug: hasValue(slug) ? slug : null, base: hasValue(base) ? base : null, preset: hasValue(preset) ? preset : null, json, cwd, env, resolve, adviceClient, out });
|
|
114
109
|
}
|
|
110
|
+
if (Array.isArray(recordPath)) return usage('--path may be given only once', json, out);
|
|
115
111
|
if (hasValue(base)) return usage('--base is valid only with --action start', json, out);
|
|
116
112
|
if (hasValue(preset)) return usage('--preset is valid only with --action start', json, out);
|
|
117
113
|
if (hasValue(slug) && hasValue(recordPath)) return usage('use either --slug or --path, not both', json, out);
|