@uniqbit/mate-core 0.18.5 → 1.0.0-canary.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/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +177 -138
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +176 -3
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +5 -13
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +4 -0
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +9 -17
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +48 -40
- package/src/lib/orchestrator/adapters/opencode.ts +2 -0
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
- package/src/lib/orchestrator/projection-entries.ts +45 -14
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -0
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +45 -5
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -1
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +15 -1
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -11
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -2
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +151 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +97 -21
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +35 -0
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +284 -8
- package/src/tools/setup/providers/opencode.ts +180 -1
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +126 -10
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/tools/setup/runtime-documents.ts +0 -291
|
@@ -3,16 +3,41 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
isPublishable,
|
|
8
|
+
legacyDeliveryWarning,
|
|
9
|
+
readDeliveryState,
|
|
10
|
+
} from "../../../../lib/delivery/record";
|
|
11
|
+
import {
|
|
12
|
+
ARCHIVE_RELATIVE_DIR,
|
|
13
|
+
archiveAnchors,
|
|
14
|
+
archiveDirOf,
|
|
15
|
+
CHANGES_RELATIVE_DIR,
|
|
16
|
+
deltaSpecs,
|
|
17
|
+
isChangeName,
|
|
18
|
+
parseAnchor,
|
|
19
|
+
SPECS_RELATIVE_DIR,
|
|
20
|
+
} from "../../../../lib/change-layout";
|
|
21
|
+
import { FRONTMATTER_PATTERN } from "../../../../lib/frontmatter";
|
|
22
|
+
import { pathIsDirectory } from "../../../../lib/fs-utils";
|
|
23
|
+
import {
|
|
24
|
+
BOM,
|
|
25
|
+
DELTA_COMMENT_PATTERN,
|
|
26
|
+
parseScopes,
|
|
27
|
+
type ScopePair,
|
|
28
|
+
} from "../../../../lib/openspec-scopes";
|
|
6
29
|
import { hasOpenspecCapability } from "../../../../lib/orchestrator/capabilities";
|
|
7
30
|
import { runIndexCapCommand } from "../../cap/index-cmd";
|
|
8
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
canonicalSpecsFor,
|
|
33
|
+
discoverArchives,
|
|
34
|
+
pendingArchives,
|
|
35
|
+
unattributedSpecs,
|
|
36
|
+
type ArchiveEntry,
|
|
37
|
+
} from "../pending/discovery";
|
|
9
38
|
import type { ArtifactFinisher, FinishContext, Produced, ResolveResult } from "./finisher";
|
|
10
39
|
import type { GitOps } from "./git";
|
|
11
40
|
|
|
12
|
-
function escapeRegExp(value: string): string {
|
|
13
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
41
|
async function pathExists(candidate: string): Promise<boolean> {
|
|
17
42
|
try {
|
|
18
43
|
await fs.stat(candidate);
|
|
@@ -22,9 +47,6 @@ async function pathExists(candidate: string): Promise<boolean> {
|
|
|
22
47
|
}
|
|
23
48
|
}
|
|
24
49
|
|
|
25
|
-
/** A dated archive anchor; group 1 is the change name the archive was created from. */
|
|
26
|
-
const ANCHOR_PATTERN = /^(\d{4}-\d{2}-\d{2})-(.+)$/;
|
|
27
|
-
|
|
28
50
|
/**
|
|
29
51
|
* The publication commit's exact scope. The active-change path is included only when it
|
|
30
52
|
* is gone from disk — that absence IS the deletion archiving produced. A path that still
|
|
@@ -35,68 +57,23 @@ async function commitPathsForArchive(
|
|
|
35
57
|
name: string,
|
|
36
58
|
anchorName: string,
|
|
37
59
|
): Promise<string[]> {
|
|
38
|
-
const archiveRelative =
|
|
39
|
-
const activeRelative =
|
|
40
|
-
|
|
41
|
-
const canonicalSpecs
|
|
42
|
-
const collectDeltaSpecFiles = async (directory: string, relative = ""): Promise<void> => {
|
|
43
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
44
|
-
for (const entry of entries) {
|
|
45
|
-
const entryRelative = path.posix.join(relative, entry.name);
|
|
46
|
-
if (entry.isDirectory()) {
|
|
47
|
-
await collectDeltaSpecFiles(path.join(directory, entry.name), entryRelative);
|
|
48
|
-
} else if (entry.isFile()) {
|
|
49
|
-
canonicalSpecs.push(path.posix.join("openspec", "specs", entryRelative));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
try {
|
|
54
|
-
await collectDeltaSpecFiles(deltaSpecsDir);
|
|
55
|
-
} catch {
|
|
56
|
-
// Changes without delta specs still commit their active deletion and archive.
|
|
57
|
-
}
|
|
60
|
+
const archiveRelative = `${ARCHIVE_RELATIVE_DIR}/${anchorName}`;
|
|
61
|
+
const activeRelative = `${CHANGES_RELATIVE_DIR}/${name}`;
|
|
62
|
+
// Changes without delta specs still commit their active deletion and archive.
|
|
63
|
+
const canonicalSpecs = await deltaSpecs(path.join(companionPath, archiveRelative));
|
|
58
64
|
return [
|
|
59
65
|
...((await pathExists(path.join(companionPath, activeRelative))) ? [] : [activeRelative]),
|
|
60
66
|
archiveRelative,
|
|
61
|
-
...canonicalSpecs.
|
|
67
|
+
...canonicalSpecs.map((spec) => spec.canonical),
|
|
62
68
|
];
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
const BOM = "";
|
|
66
|
-
|
|
67
|
-
interface ScopePair {
|
|
68
|
-
repository: string;
|
|
69
|
-
area: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
71
|
type ProjectionResult =
|
|
73
72
|
| { ok: true; repository: string; areas: string[] }
|
|
74
73
|
| { ok: false; reason: string };
|
|
75
74
|
|
|
76
|
-
/**
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
function parseDeltaScopes(source: string): ScopePair[] {
|
|
80
|
-
const text = source.startsWith(BOM) ? source.slice(BOM.length) : source;
|
|
81
|
-
const match = FRONTMATTER_PATTERN.exec(text);
|
|
82
|
-
if (!match) return [];
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const parsed = parse(match[1]) as unknown;
|
|
86
|
-
if (!parsed || typeof parsed !== "object") return [];
|
|
87
|
-
const scopes = (parsed as Record<string, unknown>).scopes;
|
|
88
|
-
if (!Array.isArray(scopes)) return [];
|
|
89
|
-
return scopes.flatMap((entry) => {
|
|
90
|
-
if (!entry || typeof entry !== "object") return [];
|
|
91
|
-
const pair = entry as Record<string, unknown>;
|
|
92
|
-
return typeof pair.repository === "string" && typeof pair.area === "string"
|
|
93
|
-
? [{ repository: pair.repository, area: pair.area }]
|
|
94
|
-
: [];
|
|
95
|
-
});
|
|
96
|
-
} catch {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
75
|
+
/** The opening line of a `mate:delta` comment, closed or not. */
|
|
76
|
+
const DELTA_COMMENT_OPENER = /^<!--[ \t]*mate:delta/;
|
|
100
77
|
|
|
101
78
|
const DELTA_TAGS = new Set(["openspec/change", "openspec/delta"]);
|
|
102
79
|
|
|
@@ -177,7 +154,8 @@ function renderCanonicalFrontmatter(
|
|
|
177
154
|
|
|
178
155
|
/**
|
|
179
156
|
* Repairs `openspec archive` output: prepends canonical frontmatter to main specs the
|
|
180
|
-
* archive run created bare, and replaces a delta-shaped header
|
|
157
|
+
* archive run created bare, and replaces a delta-shaped header or `mate:delta` comment an
|
|
158
|
+
* agent copied onto one.
|
|
181
159
|
* Publish does not archive, so this runs the first time publish resolves that archive,
|
|
182
160
|
* however the archive was made.
|
|
183
161
|
*
|
|
@@ -192,72 +170,50 @@ async function reconcileMainSpecFrontmatter(
|
|
|
192
170
|
): Promise<string[]> {
|
|
193
171
|
if (!(await archivedChangeUsesMateSchema(companionPath, anchorName))) return [];
|
|
194
172
|
|
|
195
|
-
const
|
|
196
|
-
companionPath,
|
|
197
|
-
"openspec",
|
|
198
|
-
"changes",
|
|
199
|
-
"archive",
|
|
200
|
-
anchorName,
|
|
201
|
-
"specs",
|
|
202
|
-
);
|
|
173
|
+
const archiveDir = path.join(archiveDirOf(companionPath), anchorName);
|
|
203
174
|
const reconciled: string[] = [];
|
|
204
175
|
|
|
205
|
-
const
|
|
206
|
-
|
|
176
|
+
for (const { file, canonical: canonicalRelative, capability } of await deltaSpecs(archiveDir)) {
|
|
177
|
+
if (capability === null) continue;
|
|
178
|
+
const canonicalPath = path.join(companionPath, canonicalRelative);
|
|
179
|
+
|
|
207
180
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
181
|
+
const canonical = (await fs.readFile(canonicalPath, "utf8")).replace(BOM, "");
|
|
182
|
+
/** The body that follows the written block: the whole file, or what trails a delta-shaped header. */
|
|
183
|
+
let body = `\n${canonical}`;
|
|
184
|
+
if (DELTA_COMMENT_OPENER.test(canonical)) {
|
|
185
|
+
const header = DELTA_COMMENT_PATTERN.exec(canonical);
|
|
186
|
+
if (!header) continue;
|
|
187
|
+
body = canonical.slice(header[0].length);
|
|
188
|
+
} else if (canonical.startsWith("---")) {
|
|
189
|
+
const header = FRONTMATTER_PATTERN.exec(canonical);
|
|
190
|
+
if (!header || !isDeltaShapedFrontmatter(header[1])) continue;
|
|
191
|
+
body = canonical.slice(header[0].length);
|
|
217
192
|
}
|
|
218
|
-
if (!entry.isFile() || entry.name !== "spec.md") continue;
|
|
219
|
-
|
|
220
|
-
const capability = path.posix.dirname(entryRelative);
|
|
221
|
-
if (capability === ".") continue;
|
|
222
|
-
const canonicalRelative = path.posix.join("openspec", "specs", entryRelative);
|
|
223
|
-
const canonicalPath = path.join(companionPath, canonicalRelative);
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
const canonical = (await fs.readFile(canonicalPath, "utf8")).replace(BOM, "");
|
|
227
|
-
/** The body that follows the written block: the whole file, or what trails a delta-shaped header. */
|
|
228
|
-
let body = `\n${canonical}`;
|
|
229
|
-
if (canonical.startsWith("---")) {
|
|
230
|
-
const header = FRONTMATTER_PATTERN.exec(canonical);
|
|
231
|
-
if (!header || !isDeltaShapedFrontmatter(header[1])) continue;
|
|
232
|
-
body = canonical.slice(header[0].length);
|
|
233
|
-
}
|
|
234
193
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
194
|
+
const deltaSource = await fs.readFile(path.join(archiveDir, "specs", file), "utf8");
|
|
195
|
+
const scopes = parseScopes(deltaSource);
|
|
196
|
+
const projected = projectDeltaScopes(scopes);
|
|
238
197
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
process.stderr.write(
|
|
242
|
-
`mate: skipped frontmatter for ${canonicalRelative}: ${projected.reason}\n`,
|
|
243
|
-
);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
const head = renderCanonicalFrontmatter(
|
|
247
|
-
capability,
|
|
248
|
-
projected.ok ? { repository: projected.repository, areas: projected.areas } : null,
|
|
249
|
-
);
|
|
250
|
-
await fs.writeFile(canonicalPath, head + body, "utf8");
|
|
251
|
-
reconciled.push(canonicalRelative);
|
|
252
|
-
} catch (error) {
|
|
198
|
+
/** Multi-repository is invalid input, so skip; absent scopes still earn a partial block. */
|
|
199
|
+
if (!projected.ok && scopes.length > 0) {
|
|
253
200
|
process.stderr.write(
|
|
254
|
-
`mate:
|
|
201
|
+
`mate: skipped frontmatter for ${canonicalRelative}: ${projected.reason}\n`,
|
|
255
202
|
);
|
|
203
|
+
continue;
|
|
256
204
|
}
|
|
205
|
+
const head = renderCanonicalFrontmatter(
|
|
206
|
+
capability,
|
|
207
|
+
projected.ok ? { repository: projected.repository, areas: projected.areas } : null,
|
|
208
|
+
);
|
|
209
|
+
await fs.writeFile(canonicalPath, head + body, "utf8");
|
|
210
|
+
reconciled.push(canonicalRelative);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
process.stderr.write(
|
|
213
|
+
`mate: could not reconcile frontmatter for ${canonicalRelative}: ${String(error)}\n`,
|
|
214
|
+
);
|
|
257
215
|
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
await walk(deltaSpecsDir);
|
|
216
|
+
}
|
|
261
217
|
return reconciled;
|
|
262
218
|
}
|
|
263
219
|
|
|
@@ -290,14 +246,102 @@ async function capSync(context: FinishContext): Promise<boolean> {
|
|
|
290
246
|
}
|
|
291
247
|
}
|
|
292
248
|
|
|
293
|
-
|
|
249
|
+
/** Why an archive's delivery keeps it unpublished, or null when it may be published. */
|
|
250
|
+
async function deliveryHold(companionPath: string, anchorName: string): Promise<string | null> {
|
|
251
|
+
const delivery = await readDeliveryState(path.join(archiveDirOf(companionPath), anchorName));
|
|
252
|
+
if (delivery.error) return `mate: refusing to publish ${anchorName}: ${delivery.error}`;
|
|
253
|
+
if (isPublishable(delivery.status)) return null;
|
|
254
|
+
const waiting = delivery.unsettled.length > 0 ? ` (${delivery.unsettled.join(", ")})` : "";
|
|
255
|
+
// The dated anchor, since a bare name resolves to an active change or the newest archive.
|
|
256
|
+
const [missing] = delivery.missing;
|
|
257
|
+
const remedy = missing
|
|
258
|
+
? `Its scopes name ${delivery.missing.join(", ")} without a delivery; record each with \`mate artifact link ${anchorName} --repository ${missing} --branch <branch>\` or \`--not-applicable <reason>\`, then publish.`
|
|
259
|
+
: `Record or settle it with \`mate artifact link ${anchorName}\`, then publish.`;
|
|
260
|
+
return `mate: ${anchorName} is held back: its delivery is ${delivery.status}${waiting}. ${remedy}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Per-run memo of each archive's canonical specs, delivery hold, and the pending archives.
|
|
265
|
+
* None changes the answer while a run publishes: a published archive was not held, so it
|
|
266
|
+
* never held another back. `publish --all` thus reads each archive and Git status once.
|
|
267
|
+
*/
|
|
268
|
+
export interface PublicationHoldCache {
|
|
269
|
+
specs: Map<string, Promise<string[]>>;
|
|
270
|
+
holds: Map<string, Promise<string | null>>;
|
|
271
|
+
pending?: Promise<ArchiveEntry[]>;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function publicationHoldCache(): PublicationHoldCache {
|
|
275
|
+
return { specs: new Map(), holds: new Map() };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function memo<T>(cache: Map<string, Promise<T>>, key: string, load: () => Promise<T>): Promise<T> {
|
|
279
|
+
let value = cache.get(key);
|
|
280
|
+
if (!value) {
|
|
281
|
+
value = load();
|
|
282
|
+
cache.set(key, value);
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The merge gate, whoever asks: an archive whose delivery is not settled stays unpublished,
|
|
289
|
+
* and so does one sharing a canonical spec with a pending held-back archive, since its
|
|
290
|
+
* commit would carry that archive's unmerged requirements along. Null when publishable.
|
|
291
|
+
*/
|
|
292
|
+
export async function publicationHold(
|
|
294
293
|
companionPath: string,
|
|
294
|
+
anchorName: string,
|
|
295
|
+
changedPaths?: () => Promise<string[]>,
|
|
296
|
+
cache: PublicationHoldCache = publicationHoldCache(),
|
|
297
|
+
): Promise<string | null> {
|
|
298
|
+
const holdOf = (anchor: string): Promise<string | null> =>
|
|
299
|
+
memo(cache.holds, anchor, () => deliveryHold(companionPath, anchor));
|
|
300
|
+
const specsOf = (anchor: string): Promise<string[]> =>
|
|
301
|
+
memo(cache.specs, anchor, () => canonicalSpecsFor(companionPath, anchor));
|
|
302
|
+
|
|
303
|
+
const own = await holdOf(anchorName);
|
|
304
|
+
if (own || !changedPaths) return own;
|
|
305
|
+
|
|
306
|
+
const specs = new Set(await specsOf(anchorName));
|
|
307
|
+
if (specs.size === 0) return null;
|
|
308
|
+
cache.pending ??= changedPaths().then(async (changed) =>
|
|
309
|
+
pendingArchives(await discoverArchives(companionPath, changed)),
|
|
310
|
+
);
|
|
311
|
+
const others = (await cache.pending).filter((entry) => entry.anchor !== anchorName);
|
|
312
|
+
for (const other of others) {
|
|
313
|
+
const shared = (await specsOf(other.anchor)).filter((spec) => specs.has(spec));
|
|
314
|
+
if (shared.length === 0) continue;
|
|
315
|
+
if ((await holdOf(other.anchor)) === null) continue;
|
|
316
|
+
return `mate: ${anchorName} is held back: ${shared.join(", ")} also carries edits from ${other.anchor}, which is not publishable yet. Publish it after ${other.anchor} settles.`;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function resolved(
|
|
322
|
+
context: FinishContext,
|
|
295
323
|
name: string,
|
|
296
324
|
anchorName: string,
|
|
297
325
|
): Promise<ResolveResult> {
|
|
326
|
+
const { companionPath } = context;
|
|
327
|
+
const hold = await publicationHold(
|
|
328
|
+
companionPath,
|
|
329
|
+
anchorName,
|
|
330
|
+
context.changedPaths,
|
|
331
|
+
context.holdCache,
|
|
332
|
+
);
|
|
333
|
+
if (hold) return { ok: false, message: hold };
|
|
334
|
+
const delivery = await readDeliveryState(path.join(archiveDirOf(companionPath), anchorName));
|
|
298
335
|
const commitPaths = await commitPathsForArchive(companionPath, name, anchorName);
|
|
299
336
|
await reconcileMainSpecFrontmatter(companionPath, anchorName);
|
|
300
|
-
return {
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
resolved: {
|
|
340
|
+
anchorName,
|
|
341
|
+
commitPaths,
|
|
342
|
+
...(delivery.status === "legacy" ? { warning: legacyDeliveryWarning(anchorName) } : {}),
|
|
343
|
+
},
|
|
344
|
+
};
|
|
301
345
|
}
|
|
302
346
|
|
|
303
347
|
/** Tag namespace segregating spec publications from dated change anchors. */
|
|
@@ -468,35 +512,30 @@ export function openspecFinisher(contextOrPath: FinishContext | string): Artifac
|
|
|
468
512
|
return hasOpenspecCapability(capabilities);
|
|
469
513
|
},
|
|
470
514
|
async resolve(target) {
|
|
471
|
-
|
|
472
|
-
|
|
515
|
+
/** A path-like target names no archive; refuse before touching the filesystem. */
|
|
516
|
+
if (!isChangeName(target)) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
message: `mate: no archived change matches "${target}". Pass a change name or a dated archive anchor.`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const anchor = parseAnchor(target);
|
|
473
523
|
|
|
474
524
|
/** A dated anchor names exactly one directory; no name matching can widen it. */
|
|
475
525
|
if (anchor) {
|
|
476
|
-
if (!(await
|
|
526
|
+
if (!(await pathIsDirectory(path.join(archiveDirOf(companionPath), target)))) {
|
|
477
527
|
return {
|
|
478
528
|
ok: false,
|
|
479
|
-
message: `mate: no archive at
|
|
529
|
+
message: `mate: no archive at ${ARCHIVE_RELATIVE_DIR}/${target}. Archive the change first with \`openspec archive ${anchor.name}\`.`,
|
|
480
530
|
};
|
|
481
531
|
}
|
|
482
|
-
return resolved(
|
|
532
|
+
return resolved(context, anchor.name, target);
|
|
483
533
|
}
|
|
484
534
|
|
|
485
|
-
const
|
|
486
|
-
let matches: string[] = [];
|
|
487
|
-
try {
|
|
488
|
-
const entries = await fs.readdir(archiveRoot, { withFileTypes: true });
|
|
489
|
-
matches = [];
|
|
490
|
-
for (const entry of entries) {
|
|
491
|
-
if (entry.isDirectory() && pattern.test(entry.name)) matches.push(entry.name);
|
|
492
|
-
}
|
|
493
|
-
matches = matches.toSorted();
|
|
494
|
-
} catch {
|
|
495
|
-
/** No archive directory at all: nothing can match. */
|
|
496
|
-
}
|
|
535
|
+
const matches = await archiveAnchors(companionPath, target);
|
|
497
536
|
|
|
498
537
|
if (matches.length === 0) {
|
|
499
|
-
const active = await pathExists(path.join(companionPath,
|
|
538
|
+
const active = await pathExists(path.join(companionPath, CHANGES_RELATIVE_DIR, target));
|
|
500
539
|
return {
|
|
501
540
|
ok: false,
|
|
502
541
|
message: active
|
|
@@ -511,7 +550,7 @@ export function openspecFinisher(contextOrPath: FinishContext | string): Artifac
|
|
|
511
550
|
message: `mate: "${target}" matches ${matches.length} archives (${matches.join(", ")}). Pass one anchor explicitly.`,
|
|
512
551
|
};
|
|
513
552
|
}
|
|
514
|
-
return resolved(
|
|
553
|
+
return resolved(context, target, matches[0]);
|
|
515
554
|
},
|
|
516
555
|
capSync() {
|
|
517
556
|
return capSync(context);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { resolveSecretPolicy } from "../../../../runtime/policy";
|
|
4
|
+
import { isSecretFile } from "../../../../runtime/secret-patterns";
|
|
5
|
+
import { listFiles, scanFiles, type SecretFinding } from "../../../../runtime/secret-scan";
|
|
6
|
+
|
|
7
|
+
/** Rule id reported for a staged path that is itself a secret file. */
|
|
8
|
+
export const SECRET_FILE_RULE_ID = "mate/secret-file";
|
|
9
|
+
|
|
10
|
+
export type PublicationSecretScan =
|
|
11
|
+
| { skipped: true }
|
|
12
|
+
| { skipped: false; findings: SecretFinding[] };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scans exactly what a publication would stage: every file under the commit
|
|
16
|
+
* paths that exists on disk (a deleted path has nothing to leak). A staged
|
|
17
|
+
* secret file counts as a finding of its own. Paths are reported relative to
|
|
18
|
+
* the companion; values never leave this function. Throws when the scanner
|
|
19
|
+
* fails, which the engine treats like a finding.
|
|
20
|
+
*/
|
|
21
|
+
export async function scanPublication(
|
|
22
|
+
companionPath: string,
|
|
23
|
+
commitPaths: readonly string[],
|
|
24
|
+
): Promise<PublicationSecretScan> {
|
|
25
|
+
const policy = resolveSecretPolicy(companionPath);
|
|
26
|
+
if (!policy.guardrails) return { skipped: true };
|
|
27
|
+
|
|
28
|
+
const files = (
|
|
29
|
+
await Promise.all(
|
|
30
|
+
commitPaths.map((commitPath) => listFiles(path.join(companionPath, commitPath))),
|
|
31
|
+
)
|
|
32
|
+
).flat();
|
|
33
|
+
const relative = (file: string) => path.relative(companionPath, file).split(path.sep).join("/");
|
|
34
|
+
const secretFiles: SecretFinding[] = files
|
|
35
|
+
.map(relative)
|
|
36
|
+
.filter((file) => isSecretFile(file, policy))
|
|
37
|
+
.map((file) => ({ path: file, line: 0, ruleId: SECRET_FILE_RULE_ID }));
|
|
38
|
+
const contentFindings = (await scanFiles(files)).map((finding) => ({
|
|
39
|
+
...finding,
|
|
40
|
+
path: relative(finding.path),
|
|
41
|
+
}));
|
|
42
|
+
return { skipped: false, findings: [...secretFiles, ...contentFindings] };
|
|
43
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { readDeliveryState, type DeliveryState } from "../../../../lib/delivery/record";
|
|
1
4
|
import { hasOpenspecCapability } from "../../../../lib/orchestrator/capabilities";
|
|
2
5
|
import { resolveForCapability } from "../../../../lib/orchestrator/framework-context";
|
|
3
6
|
import type { LaunchContext } from "../../../../lib/orchestrator/framework-context";
|
|
@@ -39,12 +42,20 @@ export interface AllCoverage {
|
|
|
39
42
|
coveredByAll: boolean;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The archive's Delivery Record and its settlement; held back unless `isPublishable`.
|
|
47
|
+
* `error` names an unreadable record.
|
|
48
|
+
*/
|
|
49
|
+
export interface PendingDelivery {
|
|
50
|
+
delivery: DeliveryState;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
/** Machine-readable result emitted with `--json`. */
|
|
43
54
|
export interface PendingResult {
|
|
44
55
|
type: string;
|
|
45
56
|
companionPath: string;
|
|
46
57
|
count: number;
|
|
47
|
-
pending: Array<ArchiveEntry & AllCoverage>;
|
|
58
|
+
pending: Array<ArchiveEntry & AllCoverage & PendingDelivery>;
|
|
48
59
|
/** Uncommitted canonical specs no pending change accounts for, each with its attribution. */
|
|
49
60
|
unattributedSpecs: Array<UnattributedSpec & AllCoverage>;
|
|
50
61
|
}
|
|
@@ -80,6 +91,23 @@ function renderUnattributed(specs: Array<UnattributedSpec & AllCoverage>): strin
|
|
|
80
91
|
];
|
|
81
92
|
}
|
|
82
93
|
|
|
94
|
+
async function withDelivery<T extends ArchiveEntry>(
|
|
95
|
+
companionPath: string,
|
|
96
|
+
entries: T[],
|
|
97
|
+
): Promise<Array<T & PendingDelivery>> {
|
|
98
|
+
return Promise.all(
|
|
99
|
+
entries.map(async (entry) => {
|
|
100
|
+
const delivery = await readDeliveryState(path.join(companionPath, entry.path));
|
|
101
|
+
return { ...entry, delivery };
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A damaged record is shown with its error, so it is fixed rather than re-linked over. */
|
|
107
|
+
function renderDeliveryStatus(delivery: DeliveryState): string {
|
|
108
|
+
return delivery.error === undefined ? delivery.status : `unreadable (${delivery.error})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
83
111
|
function renderHuman(result: PendingResult): string[] {
|
|
84
112
|
const pending =
|
|
85
113
|
result.count === 0
|
|
@@ -87,7 +115,7 @@ function renderHuman(result: PendingResult): string[] {
|
|
|
87
115
|
: [
|
|
88
116
|
`${result.count} archived change${result.count === 1 ? "" : "s"} pending publication:`,
|
|
89
117
|
...result.pending.flatMap((entry, index) => [
|
|
90
|
-
` ${index + 1}. ${entry.name} ${entry.tag} ${entry.path}${entry.coveredByAll ? " [--all]" : ""}`,
|
|
118
|
+
` ${index + 1}. ${entry.name} ${entry.tag} ${entry.path}${entry.coveredByAll ? " [--all]" : ""} delivery: ${renderDeliveryStatus(entry.delivery)}`,
|
|
91
119
|
...entry.uncommittedPaths.map((uncommitted) => ` ${uncommitted}`),
|
|
92
120
|
...entry.uncommittedSpecs.map((spec) => ` ${spec}`),
|
|
93
121
|
]),
|
|
@@ -164,7 +192,7 @@ export async function runArtifactPendingCommand(
|
|
|
164
192
|
type: "openspec",
|
|
165
193
|
companionPath: context.companionPath,
|
|
166
194
|
count: pending.length,
|
|
167
|
-
pending: markCoveredByAll(pending),
|
|
195
|
+
pending: await withDelivery(context.companionPath, markCoveredByAll(pending)),
|
|
168
196
|
unattributedSpecs: markCoveredByAll(
|
|
169
197
|
await unattributedSpecs(context.companionPath, changed, archives, changeKinds),
|
|
170
198
|
),
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
3
|
+
import {
|
|
4
|
+
ARCHIVE_RELATIVE_DIR,
|
|
5
|
+
archiveAnchors,
|
|
6
|
+
archiveDirOf,
|
|
7
|
+
CHANGES_RELATIVE_DIR,
|
|
8
|
+
changeNameOf,
|
|
9
|
+
deltaSpecs,
|
|
10
|
+
filesBelow,
|
|
11
|
+
SPECS_RELATIVE_DIR,
|
|
12
|
+
} from "../../../../lib/change-layout";
|
|
4
13
|
import type { WorkingTreeChange } from "../finish/git";
|
|
5
14
|
|
|
6
15
|
/** Whether an archived change still has uncommitted content in the companion working tree. */
|
|
@@ -48,13 +57,6 @@ export interface ArchiveEntry {
|
|
|
48
57
|
state: CommitState;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
/** Only dated archive directories are publishable; everything else is ignored. */
|
|
52
|
-
const ARCHIVE_ANCHOR_PATTERN = /^\d{4}-\d{2}-\d{2}-.+$/;
|
|
53
|
-
const ARCHIVE_DATE_PREFIX = /^\d{4}-\d{2}-\d{2}-/;
|
|
54
|
-
|
|
55
|
-
const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
|
|
56
|
-
export const SPECS_RELATIVE_DIR = "openspec/specs";
|
|
57
|
-
|
|
58
60
|
/** The tag `mate artifact publish` creates, never a recomputed date. */
|
|
59
61
|
export function finishMarker(anchor: string): string {
|
|
60
62
|
return `openspec/${anchor}`;
|
|
@@ -109,38 +111,16 @@ function changeKindFor(
|
|
|
109
111
|
);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
/**
|
|
113
|
-
* Every file beneath `directory`, as POSIX paths under `relativeRoot`. Reads directory
|
|
114
|
-
* entry names only, never file contents. Throws when `directory` cannot be read.
|
|
115
|
-
*/
|
|
116
|
-
async function filesUnder(directory: string, relativeRoot: string): Promise<string[]> {
|
|
117
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
118
|
-
const nested = await Promise.all(
|
|
119
|
-
entries.map(async (entry) => {
|
|
120
|
-
const entryRelative = path.posix.join(relativeRoot, entry.name);
|
|
121
|
-
if (entry.isDirectory()) return filesUnder(path.join(directory, entry.name), entryRelative);
|
|
122
|
-
return entry.isFile() ? [entryRelative] : [];
|
|
123
|
-
}),
|
|
124
|
-
);
|
|
125
|
-
return nested.flat();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
114
|
/**
|
|
129
115
|
* Canonical specs the change's delta specs applied to. Only directory entry names under
|
|
130
116
|
* the archive's `specs/` tree are read — never any file contents — so archived prose
|
|
131
117
|
* cannot influence discovery.
|
|
132
118
|
*/
|
|
133
|
-
async function canonicalSpecsFor(companionPath: string, anchor: string): Promise<string[]> {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
139
|
-
return specs.toSorted();
|
|
140
|
-
} catch {
|
|
141
|
-
// A change without delta specs still owns its archive and active directories.
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
119
|
+
export async function canonicalSpecsFor(companionPath: string, anchor: string): Promise<string[]> {
|
|
120
|
+
// A change without delta specs still owns its archive and active directories.
|
|
121
|
+
return (await deltaSpecs(path.join(archiveDirOf(companionPath), anchor))).map(
|
|
122
|
+
(spec) => spec.canonical,
|
|
123
|
+
);
|
|
144
124
|
}
|
|
145
125
|
|
|
146
126
|
/**
|
|
@@ -156,24 +136,12 @@ export async function discoverArchives(
|
|
|
156
136
|
uncommittedPaths: string[],
|
|
157
137
|
changeKinds: Readonly<Record<string, WorkingTreeChange>> = {},
|
|
158
138
|
): Promise<ArchiveEntry[]> {
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
entries = await fs.readdir(path.join(companionPath, "openspec", "changes", "archive"), {
|
|
162
|
-
withFileTypes: true,
|
|
163
|
-
});
|
|
164
|
-
} catch {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const anchors = entries
|
|
169
|
-
.filter((entry) => entry.isDirectory() && ARCHIVE_ANCHOR_PATTERN.test(entry.name))
|
|
170
|
-
.map((entry) => entry.name)
|
|
171
|
-
.toSorted();
|
|
139
|
+
const anchors = await archiveAnchors(companionPath);
|
|
172
140
|
|
|
173
141
|
return Promise.all(
|
|
174
142
|
anchors.map(async (anchor) => {
|
|
175
|
-
const name = anchor
|
|
176
|
-
const ownedPaths = [`${ARCHIVE_RELATIVE_DIR}/${anchor}`,
|
|
143
|
+
const name = changeNameOf(anchor);
|
|
144
|
+
const ownedPaths = [`${ARCHIVE_RELATIVE_DIR}/${anchor}`, `${CHANGES_RELATIVE_DIR}/${name}`];
|
|
177
145
|
const owned = matching(uncommittedPaths, ownedPaths);
|
|
178
146
|
const ownedDirectories = matchingDirectories(uncommittedPaths, ownedPaths);
|
|
179
147
|
const specs =
|
|
@@ -237,13 +205,9 @@ async function specFilesFor(
|
|
|
237
205
|
}
|
|
238
206
|
const collapsed = withoutTrailingSlash(worktreePath);
|
|
239
207
|
const root = collapsed.startsWith(`${SPECS_RELATIVE_DIR}/`) ? collapsed : SPECS_RELATIVE_DIR;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
} catch {
|
|
244
|
-
// A collapsed ancestor such as `openspec/` need not contain a spec tree at all.
|
|
245
|
-
return [];
|
|
246
|
-
}
|
|
208
|
+
// A collapsed ancestor such as `openspec/` need not contain a spec tree at all.
|
|
209
|
+
const files = await filesBelow(path.join(companionPath, root));
|
|
210
|
+
return files.map((file) => ({ path: `${root}/${file}`, kind: "new" as const }));
|
|
247
211
|
}
|
|
248
212
|
|
|
249
213
|
/**
|