@hassangameryt/freeflow 0.8.0 → 0.9.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/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +96 -149
- package/capabilities/cognitive-routing/SKILL.md +21 -129
- package/capabilities/cognitive-routing/references/coordinator.md +163 -0
- package/capabilities/cognitive-routing/references/worker.md +38 -0
- package/capabilities/compaction/SKILL.md +3 -3
- package/capabilities/compaction/references/summary-format.md +26 -15
- package/capabilities/messaging/SKILL.md +49 -0
- package/capabilities/task-memory/SKILL.md +87 -0
- package/command-surface.json +6 -54
- package/gemini-extension.json +1 -1
- package/hooks/claude/hooks.json +13 -3
- package/hooks/codex/hooks.json +2 -1
- package/hooks/shared/runtime-context.mjs +69 -77
- package/package.json +25 -8
- package/pi-extension/dist/ask-user/ask.js +108 -0
- package/pi-extension/dist/ask-user/form.js +250 -0
- package/pi-extension/dist/ask-user/questions.js +66 -0
- package/pi-extension/dist/ask-user/tool.js +124 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
- package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
- package/pi-extension/dist/cognitive-routing/config.js +3 -4
- package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
- package/pi-extension/dist/cognitive-routing/gate.js +20 -5
- package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
- package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
- package/pi-extension/dist/cognitive-routing/projection.js +23 -6
- package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
- package/pi-extension/dist/cognitive-routing/render.js +15 -15
- package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
- package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
- package/pi-extension/dist/cognitive-routing/session.js +3 -1
- package/pi-extension/dist/cognitive-routing/sources.js +29 -4
- package/pi-extension/dist/cognitive-routing/state.js +18 -8
- package/pi-extension/dist/cognitive-routing/status.js +4 -3
- package/pi-extension/dist/cognitive-routing/tools.js +29 -15
- package/pi-extension/dist/cognitive-routing/types.js +10 -1
- package/pi-extension/dist/compaction/carry.js +36 -22
- package/pi-extension/dist/compaction/config.js +4 -2
- package/pi-extension/dist/compaction/controller.js +52 -29
- package/pi-extension/dist/compaction/harness.js +19 -3
- package/pi-extension/dist/compaction/results.js +14 -13
- package/pi-extension/dist/compaction/tool.js +6 -6
- package/pi-extension/dist/host/catalog.js +20 -35
- package/pi-extension/dist/host/config-validation.js +233 -0
- package/pi-extension/dist/host/config.js +78 -91
- package/pi-extension/dist/host/inbound.js +142 -0
- package/pi-extension/dist/host/projection-tags.js +1 -1
- package/pi-extension/dist/host/prompts.js +24 -13
- package/pi-extension/dist/host/read-only-session.js +6 -1
- package/pi-extension/dist/host/request-history.js +7 -3
- package/pi-extension/dist/host/rows.js +25 -0
- package/pi-extension/dist/host/runtime-state.js +17 -2
- package/pi-extension/dist/host/section-keeper.js +85 -0
- package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
- package/pi-extension/dist/host/settings/settings-view.js +27 -8
- package/pi-extension/dist/host/status.js +19 -5
- package/pi-extension/dist/index.js +638 -72
- package/pi-extension/dist/messaging/config.js +27 -0
- package/pi-extension/dist/messaging/messaging.js +398 -0
- package/pi-extension/dist/messaging/registry.js +57 -0
- package/pi-extension/dist/messaging/render.js +90 -0
- package/pi-extension/dist/messaging/tools.js +109 -0
- package/pi-extension/dist/messaging/transport.js +101 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
- package/pi-extension/dist/task-memory/config.js +49 -0
- package/pi-extension/dist/task-memory/registry.js +119 -0
- package/pi-extension/dist/task-memory/render.js +107 -0
- package/pi-extension/dist/task-memory/runtime.js +994 -0
- package/pi-extension/dist/task-memory/tool.js +233 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
- package/pi-extension/dist/tool-execution/background.js +220 -28
- package/pi-extension/dist/tool-execution/config.js +14 -3
- package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
- package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
- package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
- package/pi-extension/dist/tool-execution/tools.js +2 -2
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +24 -14
- package/runtime/prompts/interaction-contract.md +7 -4
- package/runtime/prompts/tool-execution.md +3 -16
- package/runtime/prompts/working-method.md +16 -99
- package/skills/deliver-work/SKILL.md +76 -0
- package/skills/deliver-work/references/branch-closeout.md +134 -0
- package/skills/deliver-work/references/commit.md +83 -0
- package/skills/deliver-work/references/launch.md +94 -0
- package/skills/deliver-work/references/release.md +85 -0
- package/skills/deliver-work/scripts/commit-state.mjs +139 -0
- package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
- package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
- package/skills/deliver-work/scripts/push-route.mjs +122 -0
- package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
- package/skills/discuss-work/SKILL.md +148 -0
- package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
- package/skills/discuss-work/references/learning-action.md +36 -0
- package/skills/discuss-work/scripts/choices.mjs +101 -0
- package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
- package/skills/evaluate-behavior/SKILL.md +107 -0
- package/skills/evaluate-behavior/references/authoring.md +381 -0
- package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
- package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
- package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
- package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
- package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
- package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
- package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
- package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
- package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
- package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
- package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
- package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
- package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
- package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
- package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
- package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
- package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
- package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
- package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
- package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
- package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
- package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
- package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
- package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
- package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
- package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
- package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
- package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
- package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
- package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
- package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
- package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
- package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
- package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
- package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
- package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
- package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
- package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
- package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
- package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
- package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
- package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
- package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
- package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
- package/skills/execute-work/SKILL.md +96 -104
- package/skills/execute-work/references/code-practices.md +27 -51
- package/skills/execute-work/references/design.md +271 -0
- package/skills/execute-work/references/diagnose.md +117 -0
- package/skills/execute-work/references/migration.md +108 -0
- package/skills/execute-work/references/simplify.md +65 -0
- package/skills/execute-work/scripts/self-review.mjs +422 -0
- package/skills/execute-work/scripts/touch-map.mjs +290 -0
- package/skills/review-work/SKILL.md +89 -65
- package/skills/review-work/references/adjudication.md +94 -0
- package/skills/review-work/references/artifact.md +198 -0
- package/skills/review-work/references/evals-and-judges.md +66 -0
- package/skills/review-work/references/work.md +177 -0
- package/skills/review-work/scripts/check-brief.mjs +128 -0
- package/skills/review-work/scripts/review-packet.mjs +233 -0
- package/skills/setup-freeflow/SKILL.md +68 -17
- package/skills/track-work/SKILL.md +52 -20
- package/skills/track-work/references/handoff.md +190 -0
- package/skills/track-work/references/projects.md +51 -0
- package/skills/track-work/references/working-record-format.md +36 -18
- package/skills/track-work/scripts/lib/document.mjs +17 -13
- package/skills/track-work/scripts/lib/format.mjs +2 -0
- package/skills/track-work/scripts/lib/recover.mjs +254 -0
- package/skills/track-work/scripts/lib/registry.mjs +656 -0
- package/skills/track-work/scripts/lib/search.mjs +40 -0
- package/skills/track-work/scripts/lib/store.mjs +46 -15
- package/skills/track-work/scripts/lib/views.mjs +50 -1
- package/skills/track-work/scripts/working-record.mjs +232 -13
- package/skills/verify-work/SKILL.md +45 -37
- package/skills/verify-work/references/feature-map.md +120 -0
- package/skills/verify-work/references/runtime-evidence.md +203 -0
- package/skills/verify-work/references/test-design.md +25 -6
- package/skills/verify-work/scripts/test-changes.mjs +246 -0
- package/skills/write-docs/SKILL.md +113 -25
- package/skills/write-docs/references/documentation-shapes.md +68 -3
- package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
- package/skills/write-docs/scripts/artifact-check.mjs +148 -0
- package/skills/write-docs/scripts/docs-check.mjs +96 -6
- package/skills/write-docs/scripts/plan-lint.mjs +123 -0
- package/skills/write-skill/SKILL.md +92 -52
- package/skills/write-skill/references/activation-and-wording.md +143 -0
- package/skills/write-skill/references/placement-and-dependencies.md +154 -0
- package/skills/write-skill/scripts/lib/compare.mjs +701 -0
- package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
- package/skills/write-skill/scripts/skill-author.mjs +51 -8
- package/capabilities/cognitive-routing/references/both-mode.md +0 -36
- package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
- package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
- package/plugin.json +0 -33
- package/skills/action-selection/SKILL.md +0 -92
- package/skills/action-selection/references/trajectory-stalls.md +0 -73
- package/skills/bypass/SKILL.md +0 -153
- package/skills/commit-work/SKILL.md +0 -93
- package/skills/commit-work/references/git-state-edges.md +0 -94
- package/skills/commit-work/references/simple-push.md +0 -94
- package/skills/commit-work/references/staging-decisions.md +0 -106
- package/skills/decision-gate/SKILL.md +0 -87
- package/skills/design-for-depth/SKILL.md +0 -108
- package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
- package/skills/design-for-depth/references/implementation-decisions.md +0 -76
- package/skills/design-for-depth/references/interface-design-loop.md +0 -105
- package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
- package/skills/diagnose-failure/SKILL.md +0 -140
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
- package/skills/discuss/SKILL.md +0 -140
- package/skills/evaluate-skill/SKILL.md +0 -188
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
- package/skills/evaluate-skill/references/evaluation-design.md +0 -112
- package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
- package/skills/evaluate-skill/references/review-and-revision.md +0 -94
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
- package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
- package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
- package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
- package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
- package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
- package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
- package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
- package/skills/execute-work/references/execute-work-edges.md +0 -78
- package/skills/finish-branch/SKILL.md +0 -117
- package/skills/finish-branch/references/integration-options.md +0 -80
- package/skills/handoff/SKILL.md +0 -82
- package/skills/handoff/references/handoff-templates.md +0 -150
- package/skills/launch-work/SKILL.md +0 -116
- package/skills/launch-work/references/launch-readiness.md +0 -85
- package/skills/migration-work/SKILL.md +0 -115
- package/skills/migration-work/references/migration-lifecycle.md +0 -82
- package/skills/release-work/SKILL.md +0 -118
- package/skills/release-work/references/release-evidence.md +0 -78
- package/skills/review-artifact/SKILL.md +0 -143
- package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
- package/skills/review-work/references/adjudicate-work-review.md +0 -97
- package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
- package/skills/review-work/references/security-risk-lens.md +0 -67
- package/skills/setup-freeflow/references/activation-contract.md +0 -103
- package/skills/setup-freeflow/references/host-setup.md +0 -67
- package/skills/simplify-code/SKILL.md +0 -85
- package/skills/simplify-code/references/simplification-patterns.md +0 -70
- package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
- package/skills/verify-work/references/integration-evidence.md +0 -49
- package/skills/verify-work/references/performance-evidence.md +0 -59
- package/skills/workflow/SKILL.md +0 -171
- package/skills/workflow/references/domain-skill-composition.md +0 -45
- package/skills/write-plan/SKILL.md +0 -92
- package/skills/write-plan/references/plan-shapes.md +0 -156
- package/skills/write-skill/references/activation-boundaries.md +0 -106
- package/skills/write-skill/references/agent-first-instructions.md +0 -133
- package/skills/write-skill/references/progressive-disclosure.md +0 -129
- package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
- package/skills/write-spec/SKILL.md +0 -85
- package/skills/write-spec/references/artifact-standards.md +0 -86
- package/skills/write-spec/references/decision-records.md +0 -80
- package/skills/write-spec/references/spec-shapes.md +0 -143
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile as execFileCallback } from "node:child_process";
|
|
3
3
|
import { lstat, mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { realpathSync } from "node:fs";
|
|
4
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
|
|
@@ -30,12 +30,14 @@ async function gitCommand(root, args) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
async function assertGitSafePath(root, path) {
|
|
33
|
+
async function assertGitSafePath(root, path, { directory = false } = {}) {
|
|
34
34
|
const repository = await gitCommand(root, ["rev-parse", "--show-toplevel"]);
|
|
35
35
|
if (!repository.ok) return;
|
|
36
36
|
const repositoryRoot = canonicalPath(repository.stdout.trim());
|
|
37
37
|
const relativePath = relative(repositoryRoot, canonicalPath(path)).split(sep).join("/");
|
|
38
|
-
|
|
38
|
+
// A trailing slash lets a directory-only rule such as `.freeflow/*/` match a folder that does not exist yet.
|
|
39
|
+
const ignorePath = directory ? `${relativePath}/` : relativePath;
|
|
40
|
+
const ignored = await gitCommand(repositoryRoot, ["check-ignore", "--no-index", "-q", "--", ignorePath]);
|
|
39
41
|
if (!ignored.ok) fail("unignored-path", `Record path is not ignored by Git: ${relativePath}`);
|
|
40
42
|
const tracked = await gitCommand(repositoryRoot, ["ls-files", "--error-unmatch", "--", relativePath]);
|
|
41
43
|
if (tracked.ok) fail("tracked-path", `Record path is already tracked by Git: ${relativePath}`);
|
|
@@ -67,21 +69,49 @@ async function assertNoSymlinkPath(target, stopAt) {
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
/** The marker a project store keeps at its root; see registry.mjs. */
|
|
73
|
+
export const STORE_MARKER = "store.json";
|
|
74
|
+
|
|
75
|
+
/** In a tasks directory: the highest task number a removal took away, so numbering never goes back. */
|
|
76
|
+
export const LAST_TASK_NUMBER = ".last-task-number";
|
|
77
|
+
|
|
78
|
+
/** Whether root is a project store: its store.json is the registry's marker, not some other file of that name. */
|
|
79
|
+
function isStore(root) {
|
|
80
|
+
try {
|
|
81
|
+
const marker = JSON.parse(readFileSync(join(root, STORE_MARKER), "utf8"));
|
|
82
|
+
return marker?.version === 1 && typeof marker.project === "string";
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A repository keeps its records in .freeflow/tasks; a project store, marked by store.json, in tasks. */
|
|
70
89
|
export function taskRoot(root) {
|
|
71
|
-
return resolve(root, ".freeflow", "tasks");
|
|
90
|
+
return isStore(root) ? resolve(root, "tasks") : resolve(root, ".freeflow", "tasks");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The project store holding an absolute record path (<store>/tasks/<task>/record.md), or undefined. */
|
|
94
|
+
export function storeOf(recordPath) {
|
|
95
|
+
const store = dirname(dirname(dirname(resolve(recordPath))));
|
|
96
|
+
return basename(dirname(dirname(resolve(recordPath)))) === "tasks" && isStore(store) ? store : undefined;
|
|
72
97
|
}
|
|
73
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The path with every symlink resolved, including for a path that does not exist yet: the nearest existing ancestor is
|
|
101
|
+
* resolved and the rest appended. Git reports canonical paths, so comparisons with it need the same form.
|
|
102
|
+
*/
|
|
74
103
|
function canonicalPath(path) {
|
|
75
104
|
const candidate = resolve(path);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const parent = dirname(candidate);
|
|
80
|
-
if (parent === candidate) return candidate;
|
|
105
|
+
const missing = [];
|
|
106
|
+
let current = candidate;
|
|
107
|
+
for (;;) {
|
|
81
108
|
try {
|
|
82
|
-
return join(realpathSync(
|
|
109
|
+
return join(realpathSync(current), ...missing);
|
|
83
110
|
} catch {
|
|
84
|
-
|
|
111
|
+
const parent = dirname(current);
|
|
112
|
+
if (parent === current) return candidate;
|
|
113
|
+
missing.unshift(basename(current));
|
|
114
|
+
current = parent;
|
|
85
115
|
}
|
|
86
116
|
}
|
|
87
117
|
}
|
|
@@ -137,15 +167,16 @@ export async function readInput(inputPath, cwd = process.cwd()) {
|
|
|
137
167
|
}
|
|
138
168
|
}
|
|
139
169
|
|
|
140
|
-
async function nextTaskNumber(tasks) {
|
|
170
|
+
export async function nextTaskNumber(tasks) {
|
|
141
171
|
let entries = [];
|
|
142
172
|
try {
|
|
143
173
|
entries = await readdir(tasks, { withFileTypes: true });
|
|
144
174
|
} catch (error) {
|
|
145
175
|
if (error?.code !== "ENOENT") throw error;
|
|
146
176
|
}
|
|
147
|
-
// Numbers are never reused, so an old reference to a deleted task cannot point at a new one
|
|
148
|
-
|
|
177
|
+
// Numbers are never reused, so an old reference to a deleted task cannot point at a new one; a removal records the
|
|
178
|
+
// highest number it took away (LAST_TASK_NUMBER), since its directory is gone.
|
|
179
|
+
let highest = Number((await readFile(join(tasks, LAST_TASK_NUMBER), "utf8").catch(() => "0")).trim()) || 0;
|
|
149
180
|
for (const entry of entries) {
|
|
150
181
|
const match = /^task-(\d{3,})-/.exec(entry.name);
|
|
151
182
|
if (entry.isDirectory() && match) highest = Math.max(highest, Number(match[1]));
|
|
@@ -155,7 +186,7 @@ async function nextTaskNumber(tasks) {
|
|
|
155
186
|
|
|
156
187
|
export async function createRecordPath(root, taskName, slug) {
|
|
157
188
|
const tasks = taskRoot(root);
|
|
158
|
-
await assertGitSafePath(root, tasks);
|
|
189
|
+
await assertGitSafePath(root, tasks, { directory: true });
|
|
159
190
|
await mkdir(tasks, { recursive: true });
|
|
160
191
|
await assertNoSymlinkPath(tasks, canonicalPath(root));
|
|
161
192
|
const number = await nextTaskNumber(tasks);
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
activeDecisionBlocks,
|
|
3
|
+
blockLines,
|
|
4
|
+
fieldValue,
|
|
5
|
+
findEntity,
|
|
6
|
+
hasField,
|
|
7
|
+
parseDocument,
|
|
8
|
+
sectionContentLines,
|
|
9
|
+
} from "./document.mjs";
|
|
2
10
|
|
|
3
11
|
function trimTrailing(lines) {
|
|
4
12
|
const result = [...lines];
|
|
@@ -35,6 +43,47 @@ export function renderResume(text) {
|
|
|
35
43
|
|
|
36
44
|
chunks.push(joinChunk(document.lines.slice(current.headingIndex, current.end), newline));
|
|
37
45
|
chunks.push(joinChunk(document.lines.slice(future.headingIndex, future.end), newline));
|
|
46
|
+
const index = historyIndexLines(document);
|
|
47
|
+
if (index.length) {
|
|
48
|
+
chunks.push(
|
|
49
|
+
joinChunk(
|
|
50
|
+
[
|
|
51
|
+
"## History index",
|
|
52
|
+
"",
|
|
53
|
+
"Closed History, one line per entry. Read an entry with `view entry <ID>`.",
|
|
54
|
+
"",
|
|
55
|
+
...index,
|
|
56
|
+
],
|
|
57
|
+
newline,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
38
61
|
chunks.push(joinChunk(document.lines.slice(notes.headingIndex, document.lines.length), newline));
|
|
39
62
|
return `${chunks.filter(Boolean).join(`${newline}${newline}`)}${newline}`;
|
|
40
63
|
}
|
|
64
|
+
|
|
65
|
+
function hasCorrections(document, block) {
|
|
66
|
+
return blockLines(document, block).some((line) => /^#####\s+Corrections\s*$/.test(line));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function indexLine(document, block) {
|
|
70
|
+
const state = fieldValue(block, "State");
|
|
71
|
+
let label = `${block.id} ${state}`;
|
|
72
|
+
if (state === "superseded" && hasField(block, "Superseded by")) label += ` by ${fieldValue(block, "Superseded by")}`;
|
|
73
|
+
if (hasField(block, "Recorded")) label += ", recorded retroactively";
|
|
74
|
+
const corrected = hasCorrections(document, block) ? " (corrected)" : "";
|
|
75
|
+
return `- ${label} — ${block.title}${corrected}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function historyIndexLines(document) {
|
|
79
|
+
const { decisions, checkpoints, slices } = document.history;
|
|
80
|
+
const closedDecisions = decisions.blocks.filter((block) => fieldValue(block, "State") !== "active");
|
|
81
|
+
return [...closedDecisions, ...checkpoints.blocks, ...slices.blocks].map((block) => indexLine(document, block));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderEntries(text, ids) {
|
|
85
|
+
const document = parseDocument(text);
|
|
86
|
+
const { newline } = document;
|
|
87
|
+
const blocks = ids.map((id) => findEntity(document, id));
|
|
88
|
+
return `${blocks.map((block) => joinChunk(blockLines(document, block), newline)).join(`${newline}${newline}`)}${newline}`;
|
|
89
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { rm } from "node:fs/promises";
|
|
4
5
|
import { cwd } from "node:process";
|
|
5
|
-
import { dirname, isAbsolute, sep } from "node:path";
|
|
6
|
-
import { canonicalLines, joinLines, slugify } from "./lib/format.mjs";
|
|
6
|
+
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
7
|
+
import { WORK_AGREEMENT_HEADING, canonicalLines, joinLines, slugify } from "./lib/format.mjs";
|
|
7
8
|
import { parseDocument, validateDocument } from "./lib/document.mjs";
|
|
8
9
|
import { runTransition } from "./lib/transitions.mjs";
|
|
9
10
|
import {
|
|
@@ -12,9 +13,13 @@ import {
|
|
|
12
13
|
readInput,
|
|
13
14
|
readRecord,
|
|
14
15
|
resolveRecordPath,
|
|
16
|
+
storeOf,
|
|
15
17
|
writeInitialAtomically,
|
|
16
18
|
} from "./lib/store.mjs";
|
|
17
|
-
import { renderFull, renderResume } from "./lib/views.mjs";
|
|
19
|
+
import { renderEntries, renderFull, renderResume } from "./lib/views.mjs";
|
|
20
|
+
import { checkEvidencePointers, renderRecover } from "./lib/recover.mjs";
|
|
21
|
+
import { PROJECT_LINK, Registry, TASK_ID, removalText, startCommand } from "./lib/registry.mjs";
|
|
22
|
+
import { searchRecords } from "./lib/search.mjs";
|
|
18
23
|
|
|
19
24
|
const ROOT_HELP =
|
|
20
25
|
"--record is resolved against --root, which defaults to the current directory; an absolute --record path finds its own root.";
|
|
@@ -23,11 +28,15 @@ function usage() {
|
|
|
23
28
|
return [
|
|
24
29
|
"Usage:",
|
|
25
30
|
" working-record.mjs init --root <path> --name <task name> [--input <file|->]",
|
|
26
|
-
" working-record.mjs view resume|full --record <record.md>",
|
|
31
|
+
" working-record.mjs view resume|recover|full --record <record.md>",
|
|
32
|
+
" working-record.mjs view entry <ID> [<ID> ...] --record <record.md>",
|
|
27
33
|
" working-record.mjs validate --record <record.md>",
|
|
28
34
|
" working-record.mjs <slice|checkpoint|decision|task> <operation> ...",
|
|
35
|
+
" working-record.mjs project <new|link|show|list|remove> ...",
|
|
36
|
+
" working-record.mjs task <create|import|search|env|here|fanout|remove> ...",
|
|
29
37
|
"",
|
|
30
38
|
ROOT_HELP,
|
|
39
|
+
"A registered task's ID (such as my-project/task-003) may be given as --record.",
|
|
31
40
|
].join("\n");
|
|
32
41
|
}
|
|
33
42
|
|
|
@@ -63,16 +72,71 @@ const COMMAND_HELP = new Map([
|
|
|
63
72
|
"task",
|
|
64
73
|
[
|
|
65
74
|
"Usage: working-record.mjs task <operation> ...",
|
|
66
|
-
"Operations: task set-state.",
|
|
75
|
+
"Operations: task set-state; and for projects: task create, task import, task search, task env, task here, task fanout, task remove.",
|
|
67
76
|
"Run working-record.mjs <group> <operation> --help for options and input fields.",
|
|
68
77
|
].join("\n"),
|
|
69
78
|
],
|
|
70
79
|
[
|
|
71
80
|
"init",
|
|
72
|
-
"Usage: init --root <path> --name <task name> [--input <file|->]\nInput: Goal and Next useful action required; the seven Current Context headings and Recovery sources are accepted, and every line must sit under one of them. The record starts active. Use --input - to read Markdown fragments from stdin.",
|
|
81
|
+
"Usage: init --root <path> --name <task name> [--input <file|->]\nInput: Goal and Next useful action required; the seven Current Context headings, Work agreement (written only when given) and Recovery sources are accepted, and every line must sit under one of them. The record starts active. Use --input - to read Markdown fragments from stdin.",
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
"view",
|
|
85
|
+
"Usage: view resume|recover|full --record <record.md>\n view entry <ID> [<ID> ...] --record <record.md>\nresume: the header, Current Context, active Decisions, Current Work, Future Work, a one-line index of closed History, and Notes. recover: the resume view, then each defining artifact and Recovery source with its trigger, whether it resolves and its size, then live Git state and where it disagrees with the record: what recovery after context loss reads first. entry: the named S-NNN, C-NNN, or D-NNN entries exactly as stored. full: the file exactly as stored, valid or not.",
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"validate",
|
|
89
|
+
"Usage: validate --record <record.md> [--strict]\nChecks the record's structure, and whether the pointers in Material updates resolve: files (from the repository root or the task folder) and commits. An unresolved pointer is a warning, since it may name a planned path or a commit not fetched yet; with --strict, one on an Evidence or Result line fails. Pointers outside the repository are listed, not checked.",
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"project",
|
|
93
|
+
"Usage: working-record.mjs project <operation> ...\nOperations: project new, project link, project show, project list, project remove.\nA project groups tasks; its records live in a store under the Freeflow home (FREEFLOW_HOME, default ~/.freeflow), outside any repository. Create or link a project only when the user asks.",
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
"project new",
|
|
97
|
+
"Usage: project new --name <name> [--cwd <directory>] [--worktrees <directory>]\nCreates the project and links the repository containing --cwd (default: the current directory) to it. --worktrees is where fan-out puts children's worktrees (default: the repository's parent directory).",
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
"project link",
|
|
101
|
+
"Usage: project link --name <name> [--cwd <directory>] [--worktrees <directory>]\nLinks another repository to an existing project; --worktrees changes where fan-out puts worktrees.",
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
"task env",
|
|
105
|
+
"Usage: task env --id <task-id> [--primary <directory>] [--add-dir <directory>] [--remove-dir <directory>]\nPrints where the task's work happens: its primary directory with its current Git branch, and additional directories. The options change it; change it only when the user asks.",
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
"task here",
|
|
109
|
+
"Usage: task here [--cwd <directory>]\nPrints the registered tasks whose primary directory contains the directory, deepest first.",
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"task fanout",
|
|
113
|
+
"Usage: task fanout --parent <task-id> --title <title> --goal <goal> [--path <directory>] [--base <ref>] [--agent <command>]\nCreates a child of the task, a Git worktree for it on branch <project>/<task-NNN>-<slug> (at <worktrees>/<repo>-<slug> unless --path), copies .freeflow/local.json into it when it has none, and makes the worktree the child's directory. The user then starts a session there: --agent is the command that starts this host's agent (such as claude or codex), and the output ends with a command for the user to paste into a new terminal.",
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
"project show",
|
|
117
|
+
"Usage: project show [--cwd <directory>] [--name <project>]\nPrints a project and its tasks: ID, title, parent, and whether a session owns it. Without --name, the project linked to the repository containing --cwd.",
|
|
118
|
+
],
|
|
119
|
+
["project list", "Usage: project list\nPrints every project: name, number of tasks, and linked repositories."],
|
|
120
|
+
[
|
|
121
|
+
"project remove",
|
|
122
|
+
"Usage: project remove --name <project>\nMoves the project's store (every task's record) to the trash under the Freeflow home, removes the project and its tasks from the registry, and removes its .freeflow/project links. Git worktrees and branches stay; the output gives the commands to remove them. Run it only when the user asked to remove that project. Never empty the trash: the user does.",
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"task remove",
|
|
126
|
+
"Usage: task remove --id <task ID>\nMoves the task's record, and its children's, to the trash under the Freeflow home and removes them from the registry. Task numbers are never reused. Git worktrees and branches stay; the output gives the commands to remove them. Run it only when the user asked to remove that task. Never empty the trash: the user does.",
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
"task create",
|
|
130
|
+
"Usage: task create --title <title> --goal <goal> [--parent <task-id>] [--cwd <directory>]\nCreates a registered record in the project linked to --cwd, or with --parent a child of that task in its project, and prints its ID and path. A child has no children.",
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"task import",
|
|
134
|
+
"Usage: task import --record <record.md> --project <name>\nMoves an unregistered record's task directory into the project's store, with the next free number, and registers it. Ask the user first.",
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"task search",
|
|
138
|
+
"Usage: task search --query <text> (--id <task-id> --scope task|tree|project | --project <name>)\nPrints matching lines with the record, entry heading and state. tree is the task's root (its parent, or itself) and the root's children. Read-only.",
|
|
73
139
|
],
|
|
74
|
-
["view", "Usage: view resume|full --record <record.md>"],
|
|
75
|
-
["validate", "Usage: validate --record <record.md>"],
|
|
76
140
|
[
|
|
77
141
|
"slice propose",
|
|
78
142
|
"Usage: slice propose --record <record.md> --title <title> --input <file|->\nInput: Intended result required; Type, Expected evidence, and Dependencies optional. Commands set State; input never includes it. Use --input - to read the fragment from stdin.",
|
|
@@ -155,7 +219,7 @@ function parseOptions(args) {
|
|
|
155
219
|
continue;
|
|
156
220
|
}
|
|
157
221
|
if (options[token] !== undefined) throw new CliError(`Duplicate option: ${token}`);
|
|
158
|
-
if (token === "--help") {
|
|
222
|
+
if (token === "--help" || token === "--strict") {
|
|
159
223
|
options[token] = true;
|
|
160
224
|
continue;
|
|
161
225
|
}
|
|
@@ -172,7 +236,7 @@ function allowedOptions(command, operation) {
|
|
|
172
236
|
const withNextAction = new Set([...common, "--next-action"]);
|
|
173
237
|
if (command === "init") return new Set(["--root", "--name", "--input", "--help"]);
|
|
174
238
|
if (command === "view") return new Set(["--root", "--record", "--view", "--help"]);
|
|
175
|
-
if (command === "validate") return new Set(["--root", "--record", "--help"]);
|
|
239
|
+
if (command === "validate") return new Set(["--root", "--record", "--strict", "--help"]);
|
|
176
240
|
if (command === "slice") {
|
|
177
241
|
if (operation === "propose") return new Set([...common, "--title"]);
|
|
178
242
|
if (operation === "start" || operation === "start-direct") return new Set([...withNextAction, "--title"]);
|
|
@@ -195,6 +259,22 @@ function allowedOptions(command, operation) {
|
|
|
195
259
|
}
|
|
196
260
|
if (command === "task" && operation === "set-state")
|
|
197
261
|
return new Set(["--root", "--record", "--state", "--next-action", "--help"]);
|
|
262
|
+
if (command === "task" && operation === "create")
|
|
263
|
+
return new Set(["--title", "--goal", "--parent", "--cwd", "--help"]);
|
|
264
|
+
if (command === "task" && operation === "import") return new Set(["--record", "--project", "--help"]);
|
|
265
|
+
if (command === "task" && operation === "search")
|
|
266
|
+
return new Set(["--query", "--id", "--scope", "--project", "--help"]);
|
|
267
|
+
if (command === "task" && operation === "env")
|
|
268
|
+
return new Set(["--id", "--primary", "--add-dir", "--remove-dir", "--help"]);
|
|
269
|
+
if (command === "task" && operation === "here") return new Set(["--cwd", "--help"]);
|
|
270
|
+
if (command === "task" && operation === "fanout")
|
|
271
|
+
return new Set(["--parent", "--title", "--goal", "--path", "--base", "--cwd", "--agent", "--help"]);
|
|
272
|
+
if (command === "project" && (operation === "new" || operation === "link"))
|
|
273
|
+
return new Set(["--name", "--cwd", "--worktrees", "--help"]);
|
|
274
|
+
if (command === "project" && operation === "show") return new Set(["--cwd", "--name", "--help"]);
|
|
275
|
+
if (command === "project" && operation === "list") return new Set(["--help"]);
|
|
276
|
+
if (command === "task" && operation === "remove") return new Set(["--id", "--help"]);
|
|
277
|
+
if (command === "project" && operation === "remove") return new Set(["--name", "--help"]);
|
|
198
278
|
return null;
|
|
199
279
|
}
|
|
200
280
|
|
|
@@ -220,6 +300,8 @@ function rootOption(options) {
|
|
|
220
300
|
const marker = `${sep}.freeflow${sep}tasks${sep}`;
|
|
221
301
|
const index = record.lastIndexOf(marker);
|
|
222
302
|
if (index > 0) return record.slice(0, index);
|
|
303
|
+
const store = storeOf(record);
|
|
304
|
+
if (store) return store;
|
|
223
305
|
}
|
|
224
306
|
return cwd();
|
|
225
307
|
}
|
|
@@ -230,6 +312,7 @@ function initialContent(skeletonLines, input) {
|
|
|
230
312
|
const targets = new Map();
|
|
231
313
|
const names = [
|
|
232
314
|
"Goal",
|
|
315
|
+
WORK_AGREEMENT_HEADING,
|
|
233
316
|
"What defines this task",
|
|
234
317
|
"Settled",
|
|
235
318
|
"Tentative",
|
|
@@ -261,6 +344,9 @@ function initialContent(skeletonLines, input) {
|
|
|
261
344
|
if (/^## /.test(inputLines[index])) throw new CliError(`Unknown initialization heading: ${inputLines[index]}`);
|
|
262
345
|
}
|
|
263
346
|
const output = [...skeletonLines];
|
|
347
|
+
// The skeleton has no Work agreement heading: it is written only when the input gives one.
|
|
348
|
+
if (targets.has(WORK_AGREEMENT_HEADING))
|
|
349
|
+
output.splice(output.indexOf("### What defines this task"), 0, `### ${WORK_AGREEMENT_HEADING}`, "");
|
|
264
350
|
for (const name of names) {
|
|
265
351
|
const content = targets.get(name);
|
|
266
352
|
if (!content) continue;
|
|
@@ -299,25 +385,158 @@ async function readExisting(options) {
|
|
|
299
385
|
return { root, ...loaded };
|
|
300
386
|
}
|
|
301
387
|
|
|
388
|
+
/** Projects, registered tasks and search, through the registry under the Freeflow home. */
|
|
389
|
+
async function runRegistry(command, operation, options) {
|
|
390
|
+
const registry = new Registry();
|
|
391
|
+
const where = options["--cwd"] ?? cwd();
|
|
392
|
+
// Where the user can browse the project's records, when the project link was made.
|
|
393
|
+
const browse = async () => {
|
|
394
|
+
const link = resolve(await registry.repoRoot(where), PROJECT_LINK);
|
|
395
|
+
return existsSync(link) ? ` The user can browse its records at ${link}.` : "";
|
|
396
|
+
};
|
|
397
|
+
if (command === "project" && operation === "new")
|
|
398
|
+
return `Project ${await registry.createProject(requireOption(options, "--name"), where, options["--worktrees"])} created and linked to ${await registry.repoRoot(where)}.${await browse()}\n`;
|
|
399
|
+
if (command === "project" && operation === "link")
|
|
400
|
+
return `${await registry.repoRoot(where)} linked to project ${await registry.linkProject(requireOption(options, "--name"), where, options["--worktrees"])}.${await browse()}\n`;
|
|
401
|
+
// Removal moves records to the trash; nothing here empties it, which is the user's to do (Task 006).
|
|
402
|
+
if (command === "task" && operation === "remove")
|
|
403
|
+
return `${removalText(await registry.removeTask(requireOption(options, "--id")), registry.trash)}\n`;
|
|
404
|
+
if (command === "project" && operation === "remove")
|
|
405
|
+
return `${removalText(await registry.removeProject(requireOption(options, "--name")), registry.trash)}\n`;
|
|
406
|
+
if (command === "project" && operation === "list") {
|
|
407
|
+
const data = await registry.read();
|
|
408
|
+
const projects = Object.entries(data.projects);
|
|
409
|
+
if (!projects.length) return "No projects.\n";
|
|
410
|
+
const count = (slug) => Object.values(data.tasks).filter((task) => task.project === slug).length;
|
|
411
|
+
return `${projects.map(([slug, project]) => `- ${project.name} (${slug}): ${count(slug)} task(s); repositories ${project.repos.join(", ")}`).join("\n")}\n`;
|
|
412
|
+
}
|
|
413
|
+
if (command === "project" && operation === "show") {
|
|
414
|
+
const found = options["--name"] ? await registry.project(options["--name"]) : await registry.projectFor(where);
|
|
415
|
+
if (!found)
|
|
416
|
+
return options["--name"]
|
|
417
|
+
? `No project named ${options["--name"]}.\n`
|
|
418
|
+
: `No project is linked to ${await registry.repoRoot(where)}.\n`;
|
|
419
|
+
const lines = [`Project ${found.project.name} (${found.slug}), store ${found.project.store}`];
|
|
420
|
+
for (const { id, task } of await registry.projectTasks(found.slug))
|
|
421
|
+
lines.push(
|
|
422
|
+
`- ${id} — ${task.title} (${task.record})${task.parent ? `, child of ${task.parent}` : ""}${task.owner ? `, owned by session ${task.owner.session}` : ""}`,
|
|
423
|
+
);
|
|
424
|
+
return `${lines.join("\n")}\n`;
|
|
425
|
+
}
|
|
426
|
+
if (operation === "create") {
|
|
427
|
+
const title = requireOption(options, "--title");
|
|
428
|
+
const goal = requireOption(options, "--goal");
|
|
429
|
+
const parent = options["--parent"];
|
|
430
|
+
const slug = parent ? (await registry.task(parent))?.project : (await registry.projectFor(where))?.slug;
|
|
431
|
+
if (!slug)
|
|
432
|
+
throw new CliError(parent ? `No task ${parent}.` : `No project is linked to ${await registry.repoRoot(where)}.`);
|
|
433
|
+
const primary = parent ? undefined : await registry.repoRoot(where);
|
|
434
|
+
const created = await registry.createTask(slug, title, goal, parent, primary);
|
|
435
|
+
return `${created.id}\n${created.record}\n`;
|
|
436
|
+
}
|
|
437
|
+
if (operation === "import") {
|
|
438
|
+
const record = resolve(requireOption(options, "--record"));
|
|
439
|
+
const moved = await registry.importRecord(requireOption(options, "--project"), record);
|
|
440
|
+
return `${moved.id}\n${moved.record}\n`;
|
|
441
|
+
}
|
|
442
|
+
if (operation === "env") {
|
|
443
|
+
const id = requireOption(options, "--id");
|
|
444
|
+
if (options["--primary"] || options["--add-dir"] || options["--remove-dir"])
|
|
445
|
+
await registry.setEnvironment(id, {
|
|
446
|
+
primary: options["--primary"],
|
|
447
|
+
add: options["--add-dir"],
|
|
448
|
+
remove: options["--remove-dir"],
|
|
449
|
+
});
|
|
450
|
+
const env = await registry.environment(id);
|
|
451
|
+
if (!env.primary) return `${id} has no recorded directory.\n`;
|
|
452
|
+
const facts =
|
|
453
|
+
env.exists === false
|
|
454
|
+
? ["missing"]
|
|
455
|
+
: [env.worktree ? "Git worktree" : "", env.branch ? `branch ${env.branch}` : ""];
|
|
456
|
+
const state = facts.filter(Boolean).join(", ");
|
|
457
|
+
return `${[`primary: ${env.primary}${state ? ` (${state})` : ""}`, ...env.directories.map((dir) => `also: ${dir}`)].join("\n")}\n`;
|
|
458
|
+
}
|
|
459
|
+
if (operation === "here") {
|
|
460
|
+
const found = await registry.here(where);
|
|
461
|
+
if (!found.length) return `No registered task works in ${resolve(where)}.\n`;
|
|
462
|
+
return `${found.map(({ id, task }) => `${id} — ${task.title} (${task.environment.primary})`).join("\n")}\n`;
|
|
463
|
+
}
|
|
464
|
+
if (operation === "fanout") {
|
|
465
|
+
const result = await registry.fanout(
|
|
466
|
+
requireOption(options, "--parent"),
|
|
467
|
+
requireOption(options, "--title"),
|
|
468
|
+
requireOption(options, "--goal"),
|
|
469
|
+
{ path: options["--path"], base: options["--base"], cwd: where },
|
|
470
|
+
);
|
|
471
|
+
if (result.error) throw new CliError(result.error);
|
|
472
|
+
return `${result.id}\n${result.path}\nbranch ${result.branch}${result.copiedConfig ? "; copied .freeflow/local.json" : ""}\nNext: open a new terminal and run:\n ${startCommand(result.path, options["--agent"])}${options["--agent"] ? "" : " (then start the agent there)"}\n`;
|
|
473
|
+
}
|
|
474
|
+
if (operation === "search") {
|
|
475
|
+
const query = requireOption(options, "--query");
|
|
476
|
+
let records;
|
|
477
|
+
if (options["--id"]) records = await registry.scope(options["--id"], options["--scope"] ?? "task");
|
|
478
|
+
else {
|
|
479
|
+
const slug = (await registry.read()).projects[options["--project"] ?? ""] ? options["--project"] : undefined;
|
|
480
|
+
if (!slug) throw new CliError("Give --id with --scope, or --project with a project's slug.");
|
|
481
|
+
records = (await registry.projectTasks(slug)).map(({ id, task }) => [id, task.record]);
|
|
482
|
+
}
|
|
483
|
+
return `${await searchRecords(records, query)}\n`;
|
|
484
|
+
}
|
|
485
|
+
throw new CliError(`Unknown command: ${command} ${operation}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
302
488
|
async function runCore(command, rest) {
|
|
303
489
|
const { options, positionals } = parseOptions(rest);
|
|
304
490
|
const operation = positionals[0];
|
|
305
491
|
if (options["--help"]) return commandHelp(command, operation);
|
|
306
492
|
validateCommandOptions(command, operation, options);
|
|
493
|
+
if (
|
|
494
|
+
command === "project" ||
|
|
495
|
+
(command === "task" && ["create", "import", "search", "env", "here", "fanout", "remove"].includes(operation))
|
|
496
|
+
)
|
|
497
|
+
return runRegistry(command, operation, options);
|
|
498
|
+
// A registered task's ID stands for its record's path.
|
|
499
|
+
if (typeof options["--record"] === "string" && TASK_ID.test(options["--record"])) {
|
|
500
|
+
const task = await new Registry().task(options["--record"]);
|
|
501
|
+
if (!task) throw new CliError(`No task ${options["--record"]} in the registry.`);
|
|
502
|
+
options["--record"] = task.record;
|
|
503
|
+
}
|
|
307
504
|
if (command === "init") return initialize(options);
|
|
308
505
|
if (command === "view") {
|
|
309
|
-
if (positionals.length > 1) throw new CliError("View accepts only resume or full");
|
|
310
506
|
const view = positionals[0] ?? options["--view"];
|
|
311
|
-
if (!view || !["resume", "full"].includes(view))
|
|
507
|
+
if (!view || !["resume", "recover", "full", "entry"].includes(view))
|
|
508
|
+
throw new CliError("View must be resume, recover, full, or entry");
|
|
509
|
+
const ids = positionals.slice(1);
|
|
510
|
+
if (view === "entry") {
|
|
511
|
+
if (!ids.length) throw new CliError("View entry needs at least one S-NNN, C-NNN, or D-NNN ID");
|
|
512
|
+
const invalid = ids.find((id) => !/^[SCD]-\d{3,}$/.test(id));
|
|
513
|
+
if (invalid) throw new CliError(`Not an entry ID: ${invalid}`);
|
|
514
|
+
} else if (ids.length) throw new CliError(`View ${view} accepts no further arguments`);
|
|
312
515
|
const loaded = await readExisting(options);
|
|
313
516
|
if (view === "full") return renderFull(loaded.text);
|
|
517
|
+
if (view === "entry") return renderEntries(loaded.text, ids);
|
|
518
|
+
if (view === "recover") return renderRecover(loaded.text, { root: loaded.root, recordPath: loaded.path });
|
|
314
519
|
return renderResume(loaded.text);
|
|
315
520
|
}
|
|
316
521
|
if (command === "validate") {
|
|
317
522
|
if (positionals.length) throw new CliError("Validate does not accept positional arguments");
|
|
318
523
|
const loaded = await readExisting(options);
|
|
319
524
|
validateDocument(parseDocument(loaded.text));
|
|
320
|
-
|
|
525
|
+
const pointers = checkEvidencePointers(loaded.text, { root: loaded.root, recordPath: loaded.path });
|
|
526
|
+
const outside = pointers.outside.length
|
|
527
|
+
? `\n${pointers.outside.length} evidence pointer(s) outside the repository, not checked:\n${pointers.outside.map((each) => `- ${each}`).join("\n")}`
|
|
528
|
+
: "";
|
|
529
|
+
// Unresolved pointers warn: a record may name a planned path or a commit not fetched yet. --strict fails on those
|
|
530
|
+
// an Evidence or Result line claims exist.
|
|
531
|
+
const unresolved = options["--strict"] ? pointers.warnings : [...pointers.problems, ...pointers.warnings];
|
|
532
|
+
const warnings = unresolved.length
|
|
533
|
+
? `\n${unresolved.length} pointer(s) in Material updates do not resolve here (a planned path, or a commit not fetched?):\n${unresolved.map((each) => `- ${each}`).join("\n")}`
|
|
534
|
+
: "";
|
|
535
|
+
if (options["--strict"] && pointers.problems.length)
|
|
536
|
+
throw new CliError(
|
|
537
|
+
`Evidence pointers on Evidence or Result lines do not resolve:\n${pointers.problems.map((each) => `- ${each}`).join("\n")}${warnings}${outside}`,
|
|
538
|
+
);
|
|
539
|
+
return `valid\n${displayPath(loaded.root, loaded.path)}\n${pointers.checked} evidence pointer(s) checked${warnings}${outside}\n`;
|
|
321
540
|
}
|
|
322
541
|
if (positionals.length !== 1) throw new CliError(`Expected one operation for ${command}`);
|
|
323
542
|
const loaded = await readExisting(options);
|