@stigmer/cli 3.12.4 → 3.12.6
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/commands/agent-exec-flags.d.ts +1 -0
- package/commands/agent-exec-flags.d.ts.map +1 -1
- package/commands/agent-exec-flags.js +3 -1
- package/commands/agent-exec-flags.js.map +1 -1
- package/commands/draft.js +4 -2
- package/commands/draft.js.map +1 -1
- package/commands/run.js +4 -2
- package/commands/run.js.map +1 -1
- package/commands/seedpack.d.ts.map +1 -1
- package/commands/seedpack.js +27 -5
- package/commands/seedpack.js.map +1 -1
- package/local/seedpack/apply.d.ts +32 -3
- package/local/seedpack/apply.d.ts.map +1 -1
- package/local/seedpack/apply.js +59 -8
- package/local/seedpack/apply.js.map +1 -1
- package/package.json +5 -5
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +10 -1
- package/resources/apply/apply.js.map +1 -1
- package/resources/connect/connect.d.ts +7 -0
- package/resources/connect/connect.d.ts.map +1 -1
- package/resources/connect/connect.js +47 -30
- package/resources/connect/connect.js.map +1 -1
- package/resources/connect/display.d.ts.map +1 -1
- package/resources/connect/display.js +3 -0
- package/resources/connect/display.js.map +1 -1
- package/resources/run/agent-exec.d.ts.map +1 -1
- package/resources/run/agent-exec.js +1 -0
- package/resources/run/agent-exec.js.map +1 -1
- package/resources/run/create.d.ts +2 -1
- package/resources/run/create.d.ts.map +1 -1
- package/resources/run/create.js +11 -6
- package/resources/run/create.js.map +1 -1
- package/resources/run/prepare.d.ts +27 -1
- package/resources/run/prepare.d.ts.map +1 -1
- package/resources/run/prepare.js +41 -2
- package/resources/run/prepare.js.map +1 -1
- package/resources/task-configs.d.ts +15 -0
- package/resources/task-configs.d.ts.map +1 -0
- package/resources/task-configs.js +144 -0
- package/resources/task-configs.js.map +1 -0
- package/resources/validate.d.ts.map +1 -1
- package/resources/validate.js +8 -1
- package/resources/validate.js.map +1 -1
- package/src/commands/agent-exec-flags.ts +5 -2
- package/src/commands/draft.ts +4 -2
- package/src/commands/run.ts +4 -2
- package/src/commands/seedpack.ts +27 -5
- package/src/local/seedpack/apply.test.ts +127 -6
- package/src/local/seedpack/apply.ts +69 -9
- package/src/resources/apply/apply.ts +11 -1
- package/src/resources/connect/connect.ts +70 -44
- package/src/resources/connect/display.ts +3 -0
- package/src/resources/run/agent-exec.test.ts +1 -0
- package/src/resources/run/agent-exec.ts +1 -0
- package/src/resources/run/create.test.ts +48 -1
- package/src/resources/run/create.ts +11 -6
- package/src/resources/run/prepare.test.ts +94 -1
- package/src/resources/run/prepare.ts +66 -1
- package/src/resources/task-configs.test.ts +98 -0
- package/src/resources/task-configs.ts +159 -0
- package/src/resources/validate.test.ts +26 -0
- package/src/resources/validate.ts +10 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Typed decode of workflow task_config blocks — the offline twin of the
|
|
2
|
+
// server's `unmarshalTaskConfig` (backend/services/stigmer-server/pkg/domain/
|
|
3
|
+
// workflow/converter/unmarshal.go).
|
|
4
|
+
//
|
|
5
|
+
// The Workflow proto models `task_config` as an open google.protobuf.Struct
|
|
6
|
+
// discriminated by `kind`, so the top-level schema decode accepts ANY config
|
|
7
|
+
// payload; a real apply then decodes it into the kind's typed message and
|
|
8
|
+
// rejects what does not fit. Before this module, `stigmer validate` and
|
|
9
|
+
// `apply --dry-run` reported "valid" for configs a real apply refuses
|
|
10
|
+
// (stigmer/stigmer#778).
|
|
11
|
+
//
|
|
12
|
+
// Scope is decode-truth only: field shapes, types, enum membership, unknown
|
|
13
|
+
// fields. Semantic validation (cross-task references, expression namespaces,
|
|
14
|
+
// model registry, protovalidate required-field rules) remains the server's
|
|
15
|
+
// authority via `validateSpec`.
|
|
16
|
+
import { fromJson } from "@bufbuild/protobuf";
|
|
17
|
+
import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
|
|
18
|
+
import { AgentCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/agent_call_pb";
|
|
19
|
+
import { CallActivityTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/call_activity_pb";
|
|
20
|
+
import { EmitEventTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/emit_event_pb";
|
|
21
|
+
import { EvalTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/eval_pb";
|
|
22
|
+
import { ForTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/for_pb";
|
|
23
|
+
import { ForkTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/fork_pb";
|
|
24
|
+
import { GrpcCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/grpc_call_pb";
|
|
25
|
+
import { HttpCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/http_call_pb";
|
|
26
|
+
import { HumanInputTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/human_input_pb";
|
|
27
|
+
import { ListenTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/listen_pb";
|
|
28
|
+
import { LlmCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/llm_call_pb";
|
|
29
|
+
import { NotificationTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/notification_pb";
|
|
30
|
+
import { RaiseTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/raise_pb";
|
|
31
|
+
import { RunTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/run_pb";
|
|
32
|
+
import { SetTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/set_pb";
|
|
33
|
+
import { SwitchTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/switch_pb";
|
|
34
|
+
import { TransformTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/transform_pb";
|
|
35
|
+
import { TryTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/try_pb";
|
|
36
|
+
import { ValidateTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/validate_pb";
|
|
37
|
+
import { WaitTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/wait_pb";
|
|
38
|
+
// Exported for the drift test (task-configs.test.ts), which holds this map and
|
|
39
|
+
// the WorkflowTaskKind enum descriptor to strict bidirectional equality — the
|
|
40
|
+
// stigmer/stigmer#353 drift class: a new task kind cannot ship without a CLI
|
|
41
|
+
// schema binding, and a retired kind cannot linger here. The entries mirror
|
|
42
|
+
// the server's `unmarshalTaskConfig` kind switch one-for-one.
|
|
43
|
+
export const TASK_CONFIG_SCHEMAS = new Map([
|
|
44
|
+
[WorkflowTaskKind.set_vars, SetTaskConfigSchema],
|
|
45
|
+
[WorkflowTaskKind.http_call, HttpCallTaskConfigSchema],
|
|
46
|
+
[WorkflowTaskKind.grpc_call, GrpcCallTaskConfigSchema],
|
|
47
|
+
[WorkflowTaskKind.switch_case, SwitchTaskConfigSchema],
|
|
48
|
+
[WorkflowTaskKind.for_each, ForTaskConfigSchema],
|
|
49
|
+
[WorkflowTaskKind.fork, ForkTaskConfigSchema],
|
|
50
|
+
[WorkflowTaskKind.try_catch, TryTaskConfigSchema],
|
|
51
|
+
[WorkflowTaskKind.listen, ListenTaskConfigSchema],
|
|
52
|
+
[WorkflowTaskKind.wait, WaitTaskConfigSchema],
|
|
53
|
+
[WorkflowTaskKind.activity_call, CallActivityTaskConfigSchema],
|
|
54
|
+
[WorkflowTaskKind.raise_error, RaiseTaskConfigSchema],
|
|
55
|
+
[WorkflowTaskKind.run_workflow, RunTaskConfigSchema],
|
|
56
|
+
[WorkflowTaskKind.agent_call, AgentCallTaskConfigSchema],
|
|
57
|
+
[WorkflowTaskKind.llm_call, LlmCallTaskConfigSchema],
|
|
58
|
+
[WorkflowTaskKind.transform, TransformTaskConfigSchema],
|
|
59
|
+
[WorkflowTaskKind.human_input, HumanInputTaskConfigSchema],
|
|
60
|
+
[WorkflowTaskKind.validate, ValidateTaskConfigSchema],
|
|
61
|
+
[WorkflowTaskKind.emit_event, EmitEventTaskConfigSchema],
|
|
62
|
+
[WorkflowTaskKind.notification, NotificationTaskConfigSchema],
|
|
63
|
+
[WorkflowTaskKind.eval, EvalTaskConfigSchema],
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* Decodes every task's `task_config` into its kind's typed proto message,
|
|
67
|
+
* throwing on the first config a real apply would reject. Runs on the decoded
|
|
68
|
+
* Workflow message (after the top-level schema decode), so snake_case and
|
|
69
|
+
* json-name spellings are already normalized.
|
|
70
|
+
*
|
|
71
|
+
* A task with no `task_config` is skipped: its absence is a protovalidate
|
|
72
|
+
* required-field violation, which is the server's layer, not decode-truth.
|
|
73
|
+
*/
|
|
74
|
+
export function decodeWorkflowTaskConfigs(workflow) {
|
|
75
|
+
for (const task of workflow.spec?.tasks ?? []) {
|
|
76
|
+
if (task.taskConfig === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
const schema = TASK_CONFIG_SCHEMAS.get(task.kind);
|
|
79
|
+
if (schema === undefined)
|
|
80
|
+
continue; // unspecified/unknown kind — the server's layer reports it
|
|
81
|
+
// protobuf-es represents google.protobuf.Struct fields as plain JSON, so
|
|
82
|
+
// the Struct's content feeds the per-kind decode directly.
|
|
83
|
+
const config = normalizeTaskConfigShorthands(task.kind, task.taskConfig);
|
|
84
|
+
try {
|
|
85
|
+
fromJson(schema, config, { ignoreUnknownFields: false });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
throw new Error(`task '${task.name}' (${WorkflowTaskKind[task.kind]}): invalid task_config: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Rewrites user-friendly DSL forms to the shapes the proto decode expects —
|
|
94
|
+
* the twin of the server's `normalizeEnumShorthands` (unmarshal.go): agent_call
|
|
95
|
+
* harness shorthands ("cursor" → "HARNESS_CURSOR"), run_config service-tier/thinking-mode
|
|
96
|
+
* shorthands ("fast" → "SERVICE_TIER_FAST"), and the environment_refs kind
|
|
97
|
+
* default (an omitted kind means environment). Without this twin the offline
|
|
98
|
+
* decode would be STRICTER than a real apply and fail valid manifests.
|
|
99
|
+
*/
|
|
100
|
+
function normalizeTaskConfigShorthands(kind, config) {
|
|
101
|
+
if (kind !== WorkflowTaskKind.agent_call || !isJsonObject(config)) {
|
|
102
|
+
return config;
|
|
103
|
+
}
|
|
104
|
+
const normalized = { ...config };
|
|
105
|
+
if (typeof normalized.harness === "string") {
|
|
106
|
+
const harness = HARNESS_SHORTHANDS[normalized.harness.toLowerCase()];
|
|
107
|
+
if (harness !== undefined)
|
|
108
|
+
normalized.harness = harness;
|
|
109
|
+
}
|
|
110
|
+
if (isJsonObject(normalized.run_config)) {
|
|
111
|
+
const runConfig = { ...normalized.run_config };
|
|
112
|
+
if (typeof runConfig.service_tier === "string") {
|
|
113
|
+
const tier = SERVICE_TIER_SHORTHANDS[runConfig.service_tier.toLowerCase()];
|
|
114
|
+
if (tier !== undefined)
|
|
115
|
+
runConfig.service_tier = tier;
|
|
116
|
+
}
|
|
117
|
+
if (typeof runConfig.thinking_mode === "string") {
|
|
118
|
+
const mode = THINKING_MODE_SHORTHANDS[runConfig.thinking_mode.toLowerCase()];
|
|
119
|
+
if (mode !== undefined)
|
|
120
|
+
runConfig.thinking_mode = mode;
|
|
121
|
+
}
|
|
122
|
+
normalized.run_config = runConfig;
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(normalized.environment_refs)) {
|
|
125
|
+
normalized.environment_refs = normalized.environment_refs.map((ref) => isJsonObject(ref) && ref.kind === undefined ? { ...ref, kind: "environment" } : ref);
|
|
126
|
+
}
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
const HARNESS_SHORTHANDS = {
|
|
130
|
+
native: "HARNESS_NATIVE",
|
|
131
|
+
cursor: "HARNESS_CURSOR",
|
|
132
|
+
};
|
|
133
|
+
const SERVICE_TIER_SHORTHANDS = {
|
|
134
|
+
standard: "SERVICE_TIER_STANDARD",
|
|
135
|
+
fast: "SERVICE_TIER_FAST",
|
|
136
|
+
};
|
|
137
|
+
const THINKING_MODE_SHORTHANDS = {
|
|
138
|
+
disabled: "THINKING_MODE_DISABLED",
|
|
139
|
+
enabled: "THINKING_MODE_ENABLED",
|
|
140
|
+
};
|
|
141
|
+
function isJsonObject(value) {
|
|
142
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=task-configs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-configs.js","sourceRoot":"","sources":["../../src/resources/task-configs.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/resources/validate.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,WAAW,EAAY,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAIhF,OAAO,EAAE,eAAe,EAAE,MAAM,qFAAqF,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/resources/validate.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,WAAW,EAAY,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAIhF,OAAO,EAAE,eAAe,EAAE,MAAM,qFAAqF,CAAC;AAQtH,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,eAAe,EAAE,WAAW,CAKrE,CAAC;AAEH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,GAAG,SAAS,CAEhF;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,GAAG,IAAI,CAW/E"}
|
package/resources/validate.js
CHANGED
|
@@ -10,6 +10,7 @@ import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1
|
|
|
10
10
|
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
11
11
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
12
12
|
import { ProjectSchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/api_pb";
|
|
13
|
+
import { decodeWorkflowTaskConfigs } from "./task-configs.js";
|
|
13
14
|
// Exported for the verb/dispatch conformance suite (registry/registry.test.ts),
|
|
14
15
|
// which holds this map and the matrix's Verb.Validate promises to strict
|
|
15
16
|
// bidirectional equality — the stigmer/stigmer#353 drift class. Command code
|
|
@@ -27,6 +28,12 @@ export function schemaForValidate(kind) {
|
|
|
27
28
|
export function validateDocument(schema, document) {
|
|
28
29
|
// ignoreUnknownFields mirrors the server's lenient unmarshal: forward-compat
|
|
29
30
|
// fields a newer server adds should not fail a slightly older CLI.
|
|
30
|
-
fromJson(schema, document, { ignoreUnknownFields: true });
|
|
31
|
+
const message = fromJson(schema, document, { ignoreUnknownFields: true });
|
|
32
|
+
// Workflows carry per-kind typed configs inside an open Struct that the
|
|
33
|
+
// top-level decode cannot see into — decode them too, so validate stops
|
|
34
|
+
// passing configs a real apply rejects (stigmer/stigmer#778).
|
|
35
|
+
if (schema === WorkflowSchema) {
|
|
36
|
+
decodeWorkflowTaskConfigs(message);
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
//# sourceMappingURL=validate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/resources/validate.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,6EAA6E;AAC7E,qDAAqD;AAErD,OAAO,EAAoB,QAAQ,EAAkB,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,oDAAoD,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,wDAAwD,CAAC;AACzF,OAAO,
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/resources/validate.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,6EAA6E;AAC7E,qDAAqD;AAErD,OAAO,EAAoB,QAAQ,EAAkB,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,oDAAoD,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,MAAM,wDAAwD,CAAC;AACzF,OAAO,EAAiB,cAAc,EAAE,MAAM,uDAAuD,CAAC;AACtG,OAAO,EAAE,eAAe,EAAE,MAAM,qFAAqF,CAAC;AACtH,OAAO,EAAE,aAAa,EAAE,MAAM,sDAAsD,CAAC;AACrF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,gFAAgF;AAChF,yEAAyE;AACzE,6EAA6E;AAC7E,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAA8C,IAAI,GAAG,CAA+B;IAC/G,CAAC,eAAe,CAAC,KAAK,EAAE,WAAW,CAAC;IACpC,CAAC,eAAe,CAAC,QAAQ,EAAE,cAAc,CAAC;IAC1C,CAAC,eAAe,CAAC,UAAU,EAAE,eAAe,CAAC;IAC7C,CAAC,eAAe,CAAC,OAAO,EAAE,aAAa,CAAC;CACzC,CAAC,CAAC;AAEH,MAAM,UAAU,iBAAiB,CAAC,IAAqB;IACrD,OAAO,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,gBAAgB,CAAC,MAAmB,EAAE,QAAmB;IACvE,6EAA6E;IAC7E,mEAAmE;IACnE,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1E,wEAAwE;IACxE,wEAAwE;IACxE,8DAA8D;IAC9D,IAAI,MAAM,KAAK,cAAc,EAAE,CAAC;QAC9B,yBAAyB,CAAC,OAAmB,CAAC,CAAC;IACjD,CAAC;AACH,CAAC"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// execution flag is added here once and both surfaces pick it up.
|
|
4
4
|
|
|
5
5
|
import type { Command } from "commander";
|
|
6
|
-
import type { AgentExecFlags, RunMode, ServiceTierFlag } from "../resources/run/prepare.js";
|
|
6
|
+
import type { AgentExecFlags, RunMode, ServiceTierFlag, ThinkingFlag } from "../resources/run/prepare.js";
|
|
7
7
|
|
|
8
8
|
/** Commander collector for repeatable string options. */
|
|
9
9
|
export const collect = (value: string, previous: string[]): string[] => [...previous, value];
|
|
@@ -26,6 +26,7 @@ export interface AgentExecOptions {
|
|
|
26
26
|
autoApprove?: boolean;
|
|
27
27
|
mode?: string;
|
|
28
28
|
serviceTier?: string;
|
|
29
|
+
thinking?: string;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -52,7 +53,8 @@ export function addAgentExecFlags(command: Command, requireMessage = false): Com
|
|
|
52
53
|
.option("--model <model>", "LLM model to use (e.g. claude-sonnet-4-6)")
|
|
53
54
|
.option("--auto-approve", "automatically approve all tool executions")
|
|
54
55
|
.option("--mode <mode>", 'interaction mode: "agent" (default) or "plan" (read-only)')
|
|
55
|
-
.option("--service-tier <tier>", 'model service tier: "standard" (default) or "fast" (requires --model naming a model with a fast tier; billed at fast rates)')
|
|
56
|
+
.option("--service-tier <tier>", 'model service tier: "standard" (default) or "fast" (requires --model naming a model with a fast tier; billed at fast rates)')
|
|
57
|
+
.option("--thinking <mode>", 'extended reasoning: "disabled" (default) or "enabled" (requires --model naming a thinking-capable model; billed at base rates, uses more output tokens)');
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** Map parsed commander options onto the shared {@link AgentExecFlags} shape. */
|
|
@@ -74,5 +76,6 @@ export function toAgentExecFlags(options: AgentExecOptions): AgentExecFlags {
|
|
|
74
76
|
autoApprove: options.autoApprove === true,
|
|
75
77
|
mode: (options.mode ?? "") as RunMode,
|
|
76
78
|
serviceTier: (options.serviceTier ?? "") as ServiceTierFlag,
|
|
79
|
+
thinking: (options.thinking ?? "") as ThinkingFlag,
|
|
77
80
|
};
|
|
78
81
|
}
|
package/src/commands/draft.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// unless detached or a local workspace is in play.
|
|
9
9
|
|
|
10
10
|
import type { Command } from "commander";
|
|
11
|
-
import { ensureAuthenticated, resolveOrganization } from "../config/index.js";
|
|
11
|
+
import { ensureAuthenticated, isCloudMode, resolveOrganization } from "../config/index.js";
|
|
12
12
|
import { UsageError } from "../errors/index.js";
|
|
13
13
|
import { addAgentExecFlags, type AgentExecOptions, toAgentExecFlags } from "./agent-exec-flags.js";
|
|
14
14
|
import { globalOrg } from "./shared.js";
|
|
@@ -73,7 +73,9 @@ async function runDraft(config: DraftConfig, options: DraftFlags, command: Comma
|
|
|
73
73
|
|
|
74
74
|
const agent = await resolveDraftAgent(resolveAgentRef, client.stigmer, config);
|
|
75
75
|
|
|
76
|
-
const prepared = await prepareAgentExec(toAgentExecFlags(options), client.stigmer, org, stderrProgress()
|
|
76
|
+
const prepared = await prepareAgentExec(toAgentExecFlags(options), client.stigmer, org, stderrProgress(), {
|
|
77
|
+
cloudBackend: isCloudMode(client.config),
|
|
78
|
+
});
|
|
77
79
|
|
|
78
80
|
if (prepared.attachments.length + prepared.workspaceFileRefs.length > 0) {
|
|
79
81
|
process.stderr.write(`Attached ${prepared.attachments.length + prepared.workspaceFileRefs.length} file(s) as context\n`);
|
package/src/commands/run.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the differ) load lazily inside the action so `--help` stays fast (DD-001).
|
|
6
6
|
|
|
7
7
|
import type { Command } from "commander";
|
|
8
|
-
import { ensureAuthenticated, resolveOrganization } from "../config/index.js";
|
|
8
|
+
import { ensureAuthenticated, isCloudMode, resolveOrganization } from "../config/index.js";
|
|
9
9
|
import { UsageError } from "../errors/index.js";
|
|
10
10
|
import { interactiveBrowseEnabled } from "../resources/picker/tty.js";
|
|
11
11
|
import { addAgentExecFlags, type AgentExecOptions } from "./agent-exec-flags.js";
|
|
@@ -171,7 +171,9 @@ async function runResolvedAgent(
|
|
|
171
171
|
const { executeResolvedAgent } = await import("../resources/run/agent-exec.js");
|
|
172
172
|
const { toAgentExecFlags } = await import("./agent-exec-flags.js");
|
|
173
173
|
|
|
174
|
-
const prepared = await prepareAgentExec(toAgentExecFlags(options), client.stigmer, org, stderrProgress()
|
|
174
|
+
const prepared = await prepareAgentExec(toAgentExecFlags(options), client.stigmer, org, stderrProgress(), {
|
|
175
|
+
cloudBackend: isCloudMode(client.config),
|
|
176
|
+
});
|
|
175
177
|
await executeResolvedAgent({
|
|
176
178
|
agent,
|
|
177
179
|
prepared,
|
package/src/commands/seedpack.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// The seedpack (system agents, skills, MCP servers, workflows under the
|
|
4
4
|
// "stigmer" org) is applied automatically on `stigmer up` in local mode; these
|
|
5
5
|
// commands bootstrap it explicitly against whatever backend is configured
|
|
6
|
-
// (local or cloud). Apply is idempotent
|
|
6
|
+
// (local or cloud). Apply is content-hash idempotent: in cloud mode the truth
|
|
7
|
+
// is the stigmer.ai/seedpack-hash label on the server's seedpack Project (any
|
|
8
|
+
// machine sees the same applied state — cloud#429); in local mode it is a
|
|
9
|
+
// marker file in the data dir, which lives and dies with the local backend.
|
|
7
10
|
//
|
|
8
11
|
// Heavy modules load lazily inside the action so `--help` stays fast (DD-001).
|
|
9
12
|
|
|
@@ -50,7 +53,11 @@ async function runApply(flags: ApplyFlags): Promise<void> {
|
|
|
50
53
|
info: (line) => process.stderr.write(`${line}\n`),
|
|
51
54
|
warn: (line) => process.stderr.write(`${line}\n`),
|
|
52
55
|
},
|
|
53
|
-
{
|
|
56
|
+
{
|
|
57
|
+
markerDir: markerDir(),
|
|
58
|
+
force: flags.force === true,
|
|
59
|
+
useServerHash: isCloudMode(load()),
|
|
60
|
+
},
|
|
54
61
|
);
|
|
55
62
|
|
|
56
63
|
const label = backendLabel();
|
|
@@ -61,11 +68,26 @@ async function runApply(flags: ApplyFlags): Promise<void> {
|
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
async function runStatus(flags: OutputFlags): Promise<void> {
|
|
64
|
-
const { seedpackContentHash } = await import("../local/seedpack/apply.js");
|
|
71
|
+
const { readServerSeedpackHash, resolveSeedpackOrg, seedpackContentHash } = await import("../local/seedpack/apply.js");
|
|
65
72
|
const { readMarker } = await import("../local/seedpack/content.js");
|
|
66
73
|
|
|
67
74
|
const hash = seedpackContentHash();
|
|
68
|
-
|
|
75
|
+
|
|
76
|
+
// Cloud mode asks the server (the label on the seedpack Project) so status
|
|
77
|
+
// is truthful from ANY machine, not just the one that last ran apply.
|
|
78
|
+
let stored: string | null;
|
|
79
|
+
let storedLabel: string;
|
|
80
|
+
if (isCloudMode(load())) {
|
|
81
|
+
const { connectBackend } = await import("../backend.js");
|
|
82
|
+
const { ensureAuthenticated } = await import("../config/index.js");
|
|
83
|
+
const client = connectBackend();
|
|
84
|
+
ensureAuthenticated(client.config);
|
|
85
|
+
stored = await readServerSeedpackHash(client.stigmer, resolveSeedpackOrg());
|
|
86
|
+
storedLabel = "Applied Hash (server)";
|
|
87
|
+
} else {
|
|
88
|
+
stored = readMarker(markerDir());
|
|
89
|
+
storedLabel = "Applied Hash";
|
|
90
|
+
}
|
|
69
91
|
|
|
70
92
|
const result = CommandResult.success("Seedpack status");
|
|
71
93
|
const section = result.addSection("");
|
|
@@ -74,7 +96,7 @@ async function runStatus(flags: OutputFlags): Promise<void> {
|
|
|
74
96
|
if (stored === null) {
|
|
75
97
|
section.field("Status", "Not applied (run 'stigmer seedpack apply')");
|
|
76
98
|
} else {
|
|
77
|
-
section.field(
|
|
99
|
+
section.field(storedLabel, stored);
|
|
78
100
|
section.field("Status", stored === hash ? "Up to date" : "Outdated (run 'stigmer seedpack apply' to update)");
|
|
79
101
|
}
|
|
80
102
|
renderResult(result, resultFormat(flags));
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { Message } from "@bufbuild/protobuf";
|
|
5
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
6
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
7
|
import { buildDeclarativeItems, scanResourceFiles } from "../../resources/apply/declarative.js";
|
|
7
8
|
import type { ControllerFn } from "../../resources/apply/handlers.js";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
applySeedpack,
|
|
11
|
+
readServerSeedpackHash,
|
|
12
|
+
resolveSeedpackOrg,
|
|
13
|
+
SEEDPACK_HASH_LABEL,
|
|
14
|
+
SEEDPACK_PROJECT_SLUG,
|
|
15
|
+
seedpackContentHash,
|
|
16
|
+
} from "./apply.js";
|
|
9
17
|
import type { SeedpackContent } from "./content.js";
|
|
10
18
|
import { extractSeedpack, readMarker, resolveSeedpackContent } from "./content.js";
|
|
11
19
|
|
|
@@ -43,27 +51,43 @@ function makeContent(): SeedpackContent {
|
|
|
43
51
|
return { dir, source: "repo" };
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
// Records each applied message
|
|
47
|
-
// the same stub apply (the handlers only call `.apply`).
|
|
48
|
-
function recordingDeps() {
|
|
54
|
+
// Records each applied message (and its proto type name); every controller
|
|
55
|
+
// resolves to the same stub apply (the handlers only call `.apply`).
|
|
56
|
+
function recordingDeps(stigmer?: unknown) {
|
|
49
57
|
const applied: string[] = [];
|
|
58
|
+
const appliedMessages: Message[] = [];
|
|
50
59
|
const controller = (() => ({
|
|
51
60
|
apply: async (m: Message) => {
|
|
52
61
|
applied.push((m as { $typeName: string }).$typeName);
|
|
62
|
+
appliedMessages.push(m);
|
|
53
63
|
return m;
|
|
54
64
|
},
|
|
55
65
|
})) as unknown as ControllerFn;
|
|
56
66
|
return {
|
|
57
67
|
applied,
|
|
68
|
+
appliedMessages,
|
|
58
69
|
deps: {
|
|
59
70
|
controller,
|
|
60
|
-
stigmer: {} as never,
|
|
71
|
+
stigmer: (stigmer ?? {}) as never,
|
|
61
72
|
info: () => {},
|
|
62
73
|
warn: () => {},
|
|
63
74
|
},
|
|
64
75
|
};
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
// A Stigmer stub whose project.getByReference reports the given stored hash
|
|
79
|
+
// (null → the label is absent; "absent" → the project itself does not exist).
|
|
80
|
+
function stigmerWithServerHash(stored: string | null | "absent"): Stigmer {
|
|
81
|
+
return {
|
|
82
|
+
project: {
|
|
83
|
+
getByReference: async () => {
|
|
84
|
+
if (stored === "absent") throw new Error("not found");
|
|
85
|
+
return { metadata: { labels: stored === null ? {} : { [SEEDPACK_HASH_LABEL]: stored } } };
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
} as unknown as Stigmer;
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
let content: SeedpackContent;
|
|
68
92
|
let markerDir: string;
|
|
69
93
|
beforeEach(() => {
|
|
@@ -125,6 +149,91 @@ describe("applySeedpack", () => {
|
|
|
125
149
|
expect(result.applied).toBe(true);
|
|
126
150
|
expect(forced.applied.length).toBeGreaterThan(0);
|
|
127
151
|
});
|
|
152
|
+
|
|
153
|
+
it("stamps the content hash as a reserved label on the applied Project", async () => {
|
|
154
|
+
const { appliedMessages, deps } = recordingDeps();
|
|
155
|
+
const result = await applySeedpack(deps, { markerDir, content });
|
|
156
|
+
|
|
157
|
+
const project = appliedMessages.find((m) => (m as { $typeName: string }).$typeName.endsWith("Project")) as
|
|
158
|
+
| { metadata?: { labels: Record<string, string> } }
|
|
159
|
+
| undefined;
|
|
160
|
+
expect(project).toBeDefined();
|
|
161
|
+
expect(project?.metadata?.labels[SEEDPACK_HASH_LABEL]).toBe(result.hash);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("applySeedpack — cloud mode (useServerHash)", () => {
|
|
166
|
+
it("skips when the server's recorded hash matches, without touching the marker", async () => {
|
|
167
|
+
const hash = seedpackContentHash({ content });
|
|
168
|
+
const { applied, deps } = recordingDeps(stigmerWithServerHash(hash));
|
|
169
|
+
|
|
170
|
+
const result = await applySeedpack(deps, { markerDir, content, useServerHash: true });
|
|
171
|
+
|
|
172
|
+
expect(result.applied).toBe(false);
|
|
173
|
+
expect(applied).toHaveLength(0);
|
|
174
|
+
expect(readMarker(markerDir)).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("applies when the server records a different hash — and never writes the marker", async () => {
|
|
178
|
+
const { applied, deps } = recordingDeps(stigmerWithServerHash("sha256:0000000000000000"));
|
|
179
|
+
|
|
180
|
+
const result = await applySeedpack(deps, { markerDir, content, useServerHash: true });
|
|
181
|
+
|
|
182
|
+
expect(result.applied).toBe(true);
|
|
183
|
+
expect(applied.length).toBeGreaterThan(0);
|
|
184
|
+
// Cloud mode's record is the Project label; a marker here would be a
|
|
185
|
+
// second record of the same fact, free to drift.
|
|
186
|
+
expect(readMarker(markerDir)).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("applies when the seedpack Project does not exist yet (first bootstrap)", async () => {
|
|
190
|
+
const { applied, deps } = recordingDeps(stigmerWithServerHash("absent"));
|
|
191
|
+
|
|
192
|
+
const result = await applySeedpack(deps, { markerDir, content, useServerHash: true });
|
|
193
|
+
|
|
194
|
+
expect(result.applied).toBe(true);
|
|
195
|
+
expect(applied.length).toBeGreaterThan(0);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("ignores a stale local marker: the server is the only truth consulted", async () => {
|
|
199
|
+
const hash = seedpackContentHash({ content });
|
|
200
|
+
// A leftover marker from the pre-cloud#429 era says "up to date"…
|
|
201
|
+
const { deps: first } = recordingDeps();
|
|
202
|
+
await applySeedpack(first, { markerDir, content });
|
|
203
|
+
// …but the server has never seen this content.
|
|
204
|
+
const { applied, deps } = recordingDeps(stigmerWithServerHash(null));
|
|
205
|
+
|
|
206
|
+
const result = await applySeedpack(deps, { markerDir, content, useServerHash: true });
|
|
207
|
+
|
|
208
|
+
expect(result.applied).toBe(true);
|
|
209
|
+
expect(applied.length).toBeGreaterThan(0);
|
|
210
|
+
expect(hash).toBe(result.hash);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("re-applies when forced even if the server hash matches", async () => {
|
|
214
|
+
const hash = seedpackContentHash({ content });
|
|
215
|
+
const { applied, deps } = recordingDeps(stigmerWithServerHash(hash));
|
|
216
|
+
|
|
217
|
+
const result = await applySeedpack(deps, { markerDir, content, useServerHash: true, force: true });
|
|
218
|
+
|
|
219
|
+
expect(result.applied).toBe(true);
|
|
220
|
+
expect(applied.length).toBeGreaterThan(0);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("readServerSeedpackHash", () => {
|
|
225
|
+
it("returns the recorded label", async () => {
|
|
226
|
+
const hash = await readServerSeedpackHash(stigmerWithServerHash("sha256:abcdef0123456789"), "stigmer");
|
|
227
|
+
expect(hash).toBe("sha256:abcdef0123456789");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("returns null when the label is absent", async () => {
|
|
231
|
+
expect(await readServerSeedpackHash(stigmerWithServerHash(null), "stigmer")).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("returns null when the project cannot be read (never applied / transient failure)", async () => {
|
|
235
|
+
expect(await readServerSeedpackHash(stigmerWithServerHash("absent"), "stigmer")).toBeNull();
|
|
236
|
+
});
|
|
128
237
|
});
|
|
129
238
|
|
|
130
239
|
describe("seedpackContentHash", () => {
|
|
@@ -139,6 +248,18 @@ describe("seedpackContentHash", () => {
|
|
|
139
248
|
// CI canary manifest) must live outside the content set, never beside the
|
|
140
249
|
// McpServer YAMLs where `loadDocuments(strict)` would reject it for missing a
|
|
141
250
|
// `kind`. The synthetic fixtures above can't catch this — only real content can.
|
|
251
|
+
// The cloud-mode hash read addresses the seedpack Project by a pinned slug.
|
|
252
|
+
// The server derives the slug from the manifest's metadata.name, so this pin
|
|
253
|
+
// holds exactly as long as the real seedpack keeps that name — verify against
|
|
254
|
+
// the real content so a rename there fails here, not silently in prod.
|
|
255
|
+
describe("SEEDPACK_PROJECT_SLUG pins the real manifest name", () => {
|
|
256
|
+
it("matches seedpack/stigmer.yaml metadata.name", () => {
|
|
257
|
+
const real = resolveSeedpackContent();
|
|
258
|
+
const manifest = readFileSync(join(real.dir, "stigmer.yaml"), "utf8");
|
|
259
|
+
expect(manifest).toMatch(new RegExp(`name:\\s*${SEEDPACK_PROJECT_SLUG}\\s*$`, "m"));
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
142
263
|
describe("real seedpack content loads through the declarative scanner", () => {
|
|
143
264
|
it("scans + builds apply items without a missing-kind error", () => {
|
|
144
265
|
const real = resolveSeedpackContent();
|
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
// 2. the project (stigmer.yaml + agents/skills/mcp-servers/workflows) via the
|
|
11
11
|
// declarative reconciler, under the target org.
|
|
12
12
|
//
|
|
13
|
-
// Idempotent
|
|
13
|
+
// Idempotent, with the applied-hash truth living with the backend it describes
|
|
14
|
+
// (cloud#429): every apply stamps the content hash as a reserved label on the
|
|
15
|
+
// seedpack Project, and cloud-mode runs skip by reading that label back — so
|
|
16
|
+
// any machine (operator laptop, CI) sees the same applied state. Local mode
|
|
17
|
+
// keeps the original marker file (the local data dir lives and dies with the
|
|
18
|
+
// local backend, so a local file IS backend-scoped state there).
|
|
14
19
|
|
|
15
20
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
16
21
|
import { tmpdir } from "node:os";
|
|
17
22
|
import { join } from "node:path";
|
|
23
|
+
import { create } from "@bufbuild/protobuf";
|
|
24
|
+
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
18
25
|
import type { Stigmer } from "@stigmer/sdk";
|
|
19
26
|
import { applyItem, resolveApplyItems } from "../../resources/apply/apply.js";
|
|
20
27
|
import { applyDeclarative, detectTrack } from "../../resources/apply/declarative.js";
|
|
@@ -31,6 +38,23 @@ import {
|
|
|
31
38
|
const DEFAULT_ORG = "stigmer";
|
|
32
39
|
const ORG_ENV_VAR = "STIGMER_SEEDPACK_ORG";
|
|
33
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Reserved label on the seedpack Project recording the last applied content
|
|
43
|
+
* hash (cloud#429). Writing a NEW value requires `can_write_reserved_labels`
|
|
44
|
+
* (the seeding identity's capability); re-sending an unchanged value is an
|
|
45
|
+
* echo the cloud guard passes for any caller — so re-applies of identical
|
|
46
|
+
* content are never treated as reserved-label writes.
|
|
47
|
+
*/
|
|
48
|
+
export const SEEDPACK_HASH_LABEL = "stigmer.ai/seedpack-hash";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The seedpack Project's slug. The server derives it from the manifest's
|
|
52
|
+
* `metadata.name` in `seedpack/stigmer.yaml` — "stigmer-seedpack" is already
|
|
53
|
+
* slug-shaped, so name and slug coincide. Pinned by a test against the real
|
|
54
|
+
* content so a rename there fails loudly here.
|
|
55
|
+
*/
|
|
56
|
+
export const SEEDPACK_PROJECT_SLUG = "stigmer-seedpack";
|
|
57
|
+
|
|
34
58
|
export interface SeedpackApplyDeps {
|
|
35
59
|
/** Raw command-controller accessor (full-proto apply, preserves metadata.id). */
|
|
36
60
|
readonly controller: ControllerFn;
|
|
@@ -43,7 +67,7 @@ export interface SeedpackApplyDeps {
|
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
export interface SeedpackApplyOptions {
|
|
46
|
-
/** Directory holding the idempotency marker (
|
|
70
|
+
/** Directory holding the idempotency marker (local mode only). */
|
|
47
71
|
readonly markerDir: string;
|
|
48
72
|
/** Target org slug. Defaults to STIGMER_SEEDPACK_ORG, then "stigmer". */
|
|
49
73
|
readonly org?: string;
|
|
@@ -52,6 +76,13 @@ export interface SeedpackApplyOptions {
|
|
|
52
76
|
/** Pre-resolved content (injectable for tests); resolved on demand otherwise. */
|
|
53
77
|
readonly content?: SeedpackContent;
|
|
54
78
|
readonly home?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Cloud mode: the idempotency truth is the {@link SEEDPACK_HASH_LABEL} on the
|
|
81
|
+
* server's seedpack Project, read via {@link readServerSeedpackHash} — the
|
|
82
|
+
* local marker is neither read nor written, so a stateless machine (CI, a
|
|
83
|
+
* second operator laptop) still skips unchanged content correctly.
|
|
84
|
+
*/
|
|
85
|
+
readonly useServerHash?: boolean;
|
|
55
86
|
}
|
|
56
87
|
|
|
57
88
|
export interface SeedpackApplyResult {
|
|
@@ -77,17 +108,35 @@ export function seedpackContentHash(opts: Pick<SeedpackApplyOptions, "content" |
|
|
|
77
108
|
return hashSeedpackContent(content.dir);
|
|
78
109
|
}
|
|
79
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Read the applied content hash recorded on the backend's seedpack Project,
|
|
113
|
+
* or null when it cannot be proven (project absent — never applied — or the
|
|
114
|
+
* read failed). Null means "apply": the apply itself is idempotent server-side,
|
|
115
|
+
* so an unprovable state costs one redundant pass, never a skipped update.
|
|
116
|
+
*/
|
|
117
|
+
export async function readServerSeedpackHash(stigmer: Stigmer, org: string): Promise<string | null> {
|
|
118
|
+
try {
|
|
119
|
+
const project = await stigmer.project.getByReference({ org, slug: SEEDPACK_PROJECT_SLUG });
|
|
120
|
+
return project.metadata?.labels?.[SEEDPACK_HASH_LABEL] ?? null;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
80
126
|
/**
|
|
81
127
|
* Apply the seedpack to the backend the deps connect to. Returns `applied:false`
|
|
82
|
-
* (without touching the backend
|
|
83
|
-
* hash and `force` is not set.
|
|
128
|
+
* (without touching the backend beyond the cloud-mode hash read) when the
|
|
129
|
+
* recorded hash already matches the content hash and `force` is not set.
|
|
84
130
|
*/
|
|
85
131
|
export async function applySeedpack(deps: SeedpackApplyDeps, opts: SeedpackApplyOptions): Promise<SeedpackApplyResult> {
|
|
86
132
|
const org = resolveSeedpackOrg(opts.org);
|
|
87
133
|
const content = opts.content ?? resolveSeedpackContent({ home: opts.home });
|
|
88
134
|
const hash = hashSeedpackContent(content.dir);
|
|
89
135
|
|
|
90
|
-
|
|
136
|
+
const appliedHash = opts.useServerHash === true
|
|
137
|
+
? await readServerSeedpackHash(deps.stigmer, org)
|
|
138
|
+
: readMarker(opts.markerDir);
|
|
139
|
+
if (opts.force !== true && appliedHash === hash) {
|
|
91
140
|
return { applied: false, hash, org };
|
|
92
141
|
}
|
|
93
142
|
|
|
@@ -96,8 +145,12 @@ export async function applySeedpack(deps: SeedpackApplyDeps, opts: SeedpackApply
|
|
|
96
145
|
try {
|
|
97
146
|
extractSeedpack(content.dir, stage);
|
|
98
147
|
await applyOrganizations(deps, stage);
|
|
99
|
-
await applyProject(deps, stage, org);
|
|
100
|
-
|
|
148
|
+
await applyProject(deps, stage, org, hash);
|
|
149
|
+
// The Project label written above is the cloud-mode record; the marker
|
|
150
|
+
// stays the local-mode one. Never both — two records of one fact drift.
|
|
151
|
+
if (opts.useServerHash !== true) {
|
|
152
|
+
writeMarker(opts.markerDir, hash);
|
|
153
|
+
}
|
|
101
154
|
} finally {
|
|
102
155
|
rmSync(stage, { recursive: true, force: true });
|
|
103
156
|
}
|
|
@@ -124,11 +177,18 @@ async function applyOrganizations(deps: SeedpackApplyDeps, stageDir: string): Pr
|
|
|
124
177
|
|
|
125
178
|
// Phase 2: the project (agents, skills, MCP servers, workflows) via the shared
|
|
126
179
|
// declarative reconciler — the same path `stigmer apply` runs for a user project.
|
|
127
|
-
|
|
180
|
+
//
|
|
181
|
+
// The content hash is stamped as a reserved label on the Project HERE, in the
|
|
182
|
+
// seedpack path only — general `stigmer apply` never writes reserved labels.
|
|
183
|
+
// The stamp rides the project apply that happens anyway, so recording the hash
|
|
184
|
+
// costs no extra RPC and can never succeed while the apply failed.
|
|
185
|
+
async function applyProject(deps: SeedpackApplyDeps, stageDir: string, org: string, contentHash: string): Promise<void> {
|
|
128
186
|
const detect = detectTrack(stageDir);
|
|
129
|
-
if (detect.track !== "declarative") {
|
|
187
|
+
if (detect.track !== "declarative" || detect.project === undefined) {
|
|
130
188
|
throw new Error(`seedpack is not a declarative project (detected '${detect.track}')`);
|
|
131
189
|
}
|
|
190
|
+
detect.project.metadata ??= create(ApiResourceMetadataSchema, {});
|
|
191
|
+
detect.project.metadata.labels[SEEDPACK_HASH_LABEL] = contentHash;
|
|
132
192
|
await applyDeclarative(detect, {
|
|
133
193
|
controller: deps.controller,
|
|
134
194
|
stigmer: deps.stigmer,
|